Composite
Composite is a structural pattern that lets individual objects and groups of objects be treated through the same interface, so client code can operate on a whole tree without caring how deep it goes.
Problem
A file manager needs to compute the total size of anything a user selects: a single file, or a directory that itself contains files and other directories nested several levels deep. The obvious first attempt writes a function that checks `if (item.type === 'file')` versus `if (item.type === 'directory')` and sums up children in the directory branch.
That branch multiplies everywhere size is computed - disk-usage reports, copy previews, quota checks - and every one of them has to be kept in sync. Add a third kind of filesystem entry, like a symlink that points at a directory, and every one of those call sites needs a new branch.
Solution
Give every filesystem entry - files and directories alike - the same interface, say a `getSize()` method. A file simply returns its own size. A directory implements `getSize()` by looping over whatever it contains and summing each child's `getSize()`, regardless of whether that child is itself a file or another directory.
Because a directory calls the exact same method it exposes, the recursion handles arbitrary nesting for free. Any code that needs a size just calls `getSize()` on whatever it was handed, without ever branching on what kind of item it is.
When to Use
- Your domain is naturally a part-whole hierarchy - folders and files, menus and menu items, org charts, nested UI layouts.
- You want callers to run the same operation on a single item or an entire subtree without writing separate code paths for each case.
Real-World Examples
- **Filesystem APIs** - a `File` and a `Directory` both expose size and traversal operations; computing a folder's total size just recurses through whatever it contains.
- **DOM tree** - a text node and an element node both implement the same `Node` interface, so traversal code like `querySelectorAll` works uniformly at any depth.
- **Protobuf / JSON AST parsers** - a scalar value and a nested object both conform to one node interface, letting a single visitor function walk the entire parsed tree.
Structure
A single shared interface covers both ends of the tree. Leaf nodes implement it by computing their own answer directly; container nodes implement the very same interface by looping over their children and combining whatever those children return - including other containers.
Participants
The shared interface that both single elements and containers implement, defining the operations client code is allowed to call.
In the diagram: Component.
A terminal node with no children of its own; it answers a Component call directly instead of forwarding it anywhere.
In the diagram: File A, File B, File C.
A node that keeps a collection of child Components - leaves, other composites, or a mix - and answers a Component call by combining the results of calling it on each child.
In the diagram: Directory.
Calls Component operations on whatever object it's given, never checking whether it's holding a single element or an entire subtree.
In the diagram: Client.
class FileSystemItem {
getName() { throw new Error('Not implemented'); }
getSize() { throw new Error('Not implemented'); }
print(indent = '') { throw new Error('Not implemented'); }
}
class File extends FileSystemItem {
constructor(name, size) {
super();
this.name = name;
this.size = size;
}
getName() { return this.name; }
getSize() { return this.size; }
print(indent = '') {
console.log(`${indent}[file] ${this.name} (${this.size} KB)`);
}
}
class Directory extends FileSystemItem {
#children = [];
constructor(name) {
super();
this.name = name;
}
add(item) { this.#children.push(item); return this; }
remove(item) { this.#children = this.#children.filter(c => c !== item); }
getName() { return this.name; }
getSize() { return this.#children.reduce((sum, c) => sum + c.getSize(), 0); }
print(indent = '') {
console.log(`${indent}[dir] ${this.name}/`);
this.#children.forEach(c => c.print(indent + ' '));
}
}
const root = new Directory('project');
const src = new Directory('src');
const tests = new Directory('tests');
src.add(new File('index.js', 12)).add(new File('app.js', 34));
tests.add(new File('app.test.js', 8));
root.add(src).add(tests).add(new File('package.json', 2));
root.print();
console.log(`Total size: ${root.getSize()} KB`);
Step 1 of 6
FileSystemItem is the shared component interface
`getName`, `getSize`, `print` are declared with no logic - both leaf files and composite directories must implement the same three methods.
Step 2 of 6
File is a leaf - it has no children
`getSize()` just returns `this.size` directly, and `print()` logs one line - a leaf handles the request itself, with nothing to delegate to.
Step 3 of 6
Directory is a composite that stores children
The private `#children` array holds any mix of `File` or `Directory` items, and `add()`/`remove()` manage that collection - a directory can contain other directories.
Step 4 of 6
getSize() delegates to children and sums the results
`this.#children.reduce((sum, c) => sum + c.getSize(), 0)` calls `getSize()` on every child regardless of whether it's a `File` or another `Directory` - the composite doesn't need to know which.
Step 5 of 6
print() recurses through the whole subtree
After logging its own line, `Directory.print()` calls `c.print(indent + ' ')` on every child - each child prints itself (and its own children, if any) with deeper indentation.
Step 6 of 6
Client code treats files and directories uniformly
`root.add(src).add(tests).add(new File(...))` mixes `Directory` and `File` objects through the same `add()` call, and `root.getSize()` returns the total across the whole nested tree without any type checks.
interface FileSystemItem {
getName(): string;
getSize(): number;
print(indent?: string): void;
}
class File implements FileSystemItem {
constructor(private name: string, private size: number) {}
public getName(): string { return this.name; }
public getSize(): number { return this.size; }
public print(indent: string = ''): void {
console.log(`${indent}[file] ${this.name} (${this.size} KB)`);
}
}
class Directory implements FileSystemItem {
private children: FileSystemItem[] = [];
constructor(private name: string) {}
public add(item: FileSystemItem): this {
this.children.push(item);
return this;
}
public remove(item: FileSystemItem): void {
this.children = this.children.filter(c => c !== item);
}
public getName(): string { return this.name; }
public getSize(): number {
return this.children.reduce((sum, c) => sum + c.getSize(), 0);
}
public print(indent: string = ''): void {
console.log(`${indent}[dir] ${this.name}/`);
this.children.forEach(c => c.print(indent + ' '));
}
}
const root = new Directory('project');
const src = new Directory('src');
const tests = new Directory('tests');
src.add(new File('index.ts', 12)).add(new File('app.ts', 34));
tests.add(new File('app.test.ts', 8));
root.add(src).add(tests).add(new File('package.json', 2));
root.print();
console.log(`Total size: ${root.getSize()} KB`);
import java.util.*;
interface FileSystemItem {
String getName();
int getSize();
void print(String indent);
}
class File implements FileSystemItem {
private final String name;
private final int size;
File(String name, int size) { this.name = name; this.size = size; }
public String getName() { return name; }
public int getSize() { return size; }
public void print(String indent) {
System.out.printf("%s[file] %s (%d KB)%n", indent, name, size);
}
}
class Directory implements FileSystemItem {
private final String name;
private final List<FileSystemItem> children = new ArrayList<>();
Directory(String name) { this.name = name; }
public Directory add(FileSystemItem item) { children.add(item); return this; }
public void remove(FileSystemItem item) { children.remove(item); }
public String getName() { return name; }
public int getSize() { return children.stream().mapToInt(FileSystemItem::getSize).sum(); }
public void print(String indent) {
System.out.printf("%s[dir] %s/%n", indent, name);
children.forEach(c -> c.print(indent + " "));
}
}
Directory root = new Directory("project");
Directory src = new Directory("src");
Directory tests = new Directory("tests");
src.add(new File("index.java", 12)).add(new File("App.java", 34));
tests.add(new File("AppTest.java", 8));
root.add(src).add(tests).add(new File("pom.xml", 2));
root.print("");
System.out.println("Total size: " + root.getSize() + " KB");
interface IFileSystemItem
{
string GetName();
int GetSize();
void Print(string indent = "");
}
class File : IFileSystemItem
{
private readonly string _name;
private readonly int _size;
public File(string name, int size) { _name = name; _size = size; }
public string GetName() => _name;
public int GetSize() => _size;
public void Print(string indent = "") =>
Console.WriteLine($"{indent}[file] {_name} ({_size} KB)");
}
class Directory : IFileSystemItem
{
private readonly string _name;
private readonly List<IFileSystemItem> _children = new();
public Directory(string name) => _name = name;
public Directory Add(IFileSystemItem item) { _children.Add(item); return this; }
public void Remove(IFileSystemItem item) => _children.Remove(item);
public string GetName() => _name;
public int GetSize() => _children.Sum(c => c.GetSize());
public void Print(string indent = "")
{
Console.WriteLine($"{indent}[dir] {_name}/");
foreach (var child in _children) child.Print(indent + " ");
}
}
var root = new Directory("project");
var src = new Directory("src");
var tests = new Directory("tests");
src.Add(new File("index.cs", 12)).Add(new File("App.cs", 34));
tests.Add(new File("AppTest.cs", 8));
root.Add(src).Add(tests).Add(new File("project.csproj", 2));
root.Print();
Console.WriteLine($"Total size: {root.GetSize()} KB");
from __future__ import annotations
from abc import ABC, abstractmethod
class FileSystemItem(ABC):
@abstractmethod
def get_name(self) -> str:
pass
@abstractmethod
def get_size(self) -> int:
pass
@abstractmethod
def print(self, indent: str = '') -> None:
pass
class File(FileSystemItem):
def __init__(self, name: str, size: int) -> None:
self._name = name
self._size = size
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return self._size
def print(self, indent: str = '') -> None:
print(f"{indent}[file] {self._name} ({self._size} KB)")
class Directory(FileSystemItem):
def __init__(self, name: str) -> None:
self._name = name
self._children: list[FileSystemItem] = []
def add(self, item: FileSystemItem) -> 'Directory':
self._children.append(item)
return self
def remove(self, item: FileSystemItem) -> None:
self._children.remove(item)
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return sum(c.get_size() for c in self._children)
def print(self, indent: str = '') -> None:
print(f"{indent}[dir] {self._name}/")
for child in self._children:
child.print(indent + ' ')
root = Directory('project')
src = Directory('src')
tests = Directory('tests')
src.add(File('main.py', 12)).add(File('app.py', 34))
tests.add(File('test_app.py', 8))
root.add(src).add(tests).add(File('requirements.txt', 2))
root.print()
print(f"Total size: {root.get_size()} KB")
Step 1 of 6
FileSystemItem is the shared component interface
`get_name`, `get_size`, `print` are all `@abstractmethod` - both `File` and `Directory` must implement the same three methods to be instantiated.
Step 2 of 6
File is a leaf - it has no children
`get_size()` just returns `self._size` directly, and `print()` prints one formatted line - nothing to recurse into.
Step 3 of 6
Directory is a composite that stores children
`self._children: list[FileSystemItem]` can hold any mix of `File` or `Directory` objects, and `add()` appends to it, returning `self` for chaining.
Step 4 of 6
get_size() delegates to children and sums the results
`sum(c.get_size() for c in self._children)` calls `get_size()` on every child, whether it's a `File` or another `Directory` - the composite treats them identically.
Step 5 of 6
print() recurses through the whole subtree
After printing its own line, `Directory.print()` loops over `self._children` and calls `child.print(indent + ' ')` on each - the recursion unwinds the entire nested tree.
Step 6 of 6
Client code treats files and directories uniformly
`root.add(src).add(tests).add(File(...))` mixes `Directory` and `File` through the same `add()` call, and `root.get_size()` sums the entire nested tree with no type checks.
Click "Run" to execute this code in a sandboxed frame and see console output here.
TypeScript runs as plain JavaScript here - type annotations are stripped, not type-checked.
Click "Run" to execute this code in a sandboxed frame and see console output here.
Advantages
- Eliminates type-checking branches for tree traversal entirely - recursion through a shared interface handles any depth automatically.
- New kinds of leaves or containers slot in by implementing the Component interface, with zero changes to code that already traverses the tree.
- Client code shrinks and simplifies, since it always calls one interface regardless of whether it's handed a single object or a whole subtree.
Disadvantages
- Forcing leaves and containers to share one interface can leave leaves with methods that make no sense for them (like managing children).
- It's tempting to put container-only operations like `add`/`remove` on the shared interface, which then forces leaves to implement no-ops or throw.
- Very deep or very large trees can make simple aggregate operations expensive, since a single call may fan out recursively across the whole structure.
Question 1 of 10
In the pattern's structure, what distinguishes a Leaf from a Composite?
Correct! A Leaf is a terminal node with no children of its own that answers a Component call directly, while a Composite keeps a collection of child Components and answers by combining the results of calling it on each child.
Not quite. A Leaf is a terminal node with no children of its own that answers a Component call directly, while a Composite keeps a collection of child Components and answers by combining the results of calling it on each child.
Question 2 of 10
What goes wrong with the naive `if (item.type === 'file')` / `if (item.type === 'directory')` approach to computing sizes?
Correct! The branch multiplies everywhere size is computed - disk-usage reports, copy previews, quota checks - and every call site needs a new branch for a new entry kind.
Not quite. The branch multiplies everywhere size is computed - disk-usage reports, copy previews, quota checks - and every call site needs a new branch for a new entry kind.
Question 3 of 10
In the DOM tree real-world example, what makes uniform traversal like `querySelectorAll` possible?
Correct! A text node and an element node both implement the same Node interface, so traversal code like querySelectorAll works uniformly at any depth.
Not quite. A text node and an element node both implement the same Node interface, so traversal code like querySelectorAll works uniformly at any depth.
Question 4 of 10
Why is `add`/`remove` on the shared Component interface flagged as a risk in the cons?
Correct! It's tempting to put container-only operations like add/remove on the shared interface, which then forces leaves to implement no-ops or throw, since they have no children to manage.
Not quite. It's tempting to put container-only operations like add/remove on the shared interface, which then forces leaves to implement no-ops or throw, since they have no children to manage.
Question 5 of 10
In the file manager problem, what task does the size-computing function need to handle?
Correct! The file manager needs to compute the total size of anything a user selects: a single file, or a directory that itself contains files and other directories nested several levels deep.
Not quite. The file manager needs to compute the total size of anything a user selects: a single file, or a directory that itself contains files and other directories nested several levels deep.
Question 6 of 10
How does a directory's `getSize()` handle arbitrary nesting in the solution?
Correct! Because a directory calls the exact same method it exposes, the recursion handles arbitrary nesting for free.
Not quite. Because a directory calls the exact same method it exposes, the recursion handles arbitrary nesting for free.
Question 7 of 10
In the JavaScript implementation, how does `Directory` store its children?
Correct! The private `#children` array holds any mix of `File` or `Directory` items, and `add()`/`remove()` manage that collection - a directory can contain other directories.
Not quite. The private `#children` array holds any mix of `File` or `Directory` items, and `add()`/`remove()` manage that collection - a directory can contain other directories.
Question 8 of 10
What does Composite let client code do?
Correct! Composite lets individual objects and groups of objects be treated through the same interface, so client code can operate on a whole tree without caring how deep it goes.
Not quite. Composite lets individual objects and groups of objects be treated through the same interface, so client code can operate on a whole tree without caring how deep it goes.
Question 9 of 10
In the walkthrough, what does `Directory.print()` do after logging its own line?
Correct! After logging its own line, `Directory.print()` calls `c.print(indent + ' ')` on every child - each child prints itself, and its own children if any, with deeper indentation.
Not quite. After logging its own line, `Directory.print()` calls `c.print(indent + ' ')` on every child - each child prints itself, and its own children if any, with deeper indentation.
Question 10 of 10
According to the Client's role description, how does it interact with the tree?
Correct! The Client calls Component operations on whatever object it's given, never checking whether it's holding a single element or an entire subtree.
Not quite. The Client calls Component operations on whatever object it's given, never checking whether it's holding a single element or an entire subtree.