Composite
What This Concept Is
Composite is a structural pattern that lets clients treat individual objects and compositions of objects uniformly by putting both behind a single interface.
The structure:
- a
Componentinterface declaring operations that make sense for both leaves and groups Leafclasses implementing the operations for primitive elements- a
Compositeclass implementing the same interface by delegating to a collection of childComponents
Recursion is the point. A Composite holds Components, which may themselves be Composites. The client walks the structure without ever caring which nodes are leaves.
The problem it absorbs: code that keeps branching on "is this one thing or a group of things," spreading the same conditional through every operation.
Why It Matters Here
Composite is how you represent trees with behavior: filesystems, UI widget trees, scene graphs, menus, organizational hierarchies, expression trees. Any time an operation must recurse over a heterogeneous collection that may contain more of itself, Composite is the clean answer.
It also cleans up a specific code smell: the repeated "if leaf do X else recurse" pattern scattered across many methods.
Concrete Example
// Component
interface FsNode {
name(): string;
sizeBytes(): number; // operation meaningful for both
print(indent?: string): void;
}
// Leaf
class File implements FsNode {
constructor(private readonly _name: string, private readonly _size: number) {}
name() { return this._name; }
sizeBytes() { return this._size; }
print(indent = '') { console.log(`${indent}- ${this._name} (${this._size}B)`); }
}
// Composite
class Directory implements FsNode {
private children: FsNode[] = [];
constructor(private readonly _name: string) {}
add(n: FsNode) { this.children.push(n); }
name() { return this._name; }
sizeBytes() { return this.children.reduce((s, c) => s + c.sizeBytes(), 0); }
print(indent = '') {
console.log(`${indent}+ ${this._name}/`);
for (const c of this.children) c.print(indent + ' ');
}
}
const root = new Directory('root');
root.add(new File('readme.md', 120));
const src = new Directory('src');
src.add(new File('main.ts', 800));
root.add(src);
root.print();
console.log(`total: ${root.sizeBytes()}B`);
Structural sketch:
+-----------+
| Component |<----------+
+-----------+ |
^ ^ |
| | |children
+------+ +-----------+ |
| Leaf | | Composite |---+
+------+ +-----------+
children: [Component,...]
The recursion on the right-hand side is the whole idea.
Common Confusion / Misconception
- A tree of objects is not automatically a Composite. Composite requires that leaf and group share the same interface and that clients can treat them uniformly. Without that, you just have a tree data structure.
- Be honest about "safe vs transparent" trade-offs: putting
add(child)on theComponentinterface makes leaves technically accept children (transparent but unsafe); keepingaddonly onCompositeforces the client to distinguish (safe but less transparent). Head First calls this the safety-versus-transparency trade-off. - Composite is not Decorator. Both wrap, but Composite is a tree and Decorator is a chain of one-child wrappers.
How To Use It
- Find operations that currently branch on "single item vs group." Those are your Component methods.
- Define the Component interface with the operations both leaves and groups should support.
- Implement Leaf classes for primitives, with pure logic.
- Implement Composite with a children collection and a loop delegating to each child.
- Decide consciously whether child management lives on the Component (transparent) or only on the Composite (safe).
Check Yourself
- What is the "uniform treatment" property Composite protects, and why do clients care?
- What is the safety-versus-transparency trade-off?
- When does a tree not want Composite?
Mini Drill or Application
Model a small file system with File (leaf) and Directory (composite). Implement:
sizeBytes()that recurses.find(predicate)that walks the tree and returns all matches as a flat list.print(indent)that renders the tree.
Then describe in one paragraph when you would not use Composite -- for example, when leaf and group operations differ too much to share a single interface.