Structural pattern
Composite
Treat a single object and a whole group of objects through the exact same interface — so client code never needs to know which one it's holding.
✗ The problem
Every call site branches on the type
Client code special-cases leaves vs. containers everywhere it touches the tree.
function totalSize(node) {
if (node.isFolder) {
let sum = 0;
for (const c of node.children)
sum += totalSize(c); // recurse… correctly?
return sum;
} else {
return node.size; // leaf case
}
}
Every operation on the tree must branch on
isFolder — and it's
easy to get the recursion wrong, or forget a branch when a new node type shows up.
✓ The pattern
↓
One interface, two implementations
A Leaf returns its own size. A Composite holds children
and returns their sum — through the same size() call.
class File {
constructor(kb) { this.kb = kb; }
size() { return this.kb; }
}
class Folder {
children = [];
add(c) { this.children.push(c); }
size() {
return this.children.reduce(
(t, c) => t + c.size(), 0);
}
}
Component
size()
File
leaf
Folder
composite → children[]
✓ See it live
Click through the tree — then sum it in one call
Click a folder to expand/collapse it. Every node — file or folder — answers the same
size() call; folders just add up their children's answers.
Total: — KB
one recursive call — no type checks
✓ Takeaway
One recursive call, no type checks
- Uniform treatment: individual objects and compositions share one contract.
- Natural for trees: filesystems, the DOM, menus, org charts — anywhere "one or many".
- Client code stays flat: it calls
size()once and never asks "which kind is this?" - Careful: deep trees can mean deep recursion; watch stack depth on huge structures.
🎯 Principle applied: Composite gives leaves and containers a shared contract — Liskov Substitution (they're interchangeable) and Open/Closed (add new node types without touching client code).
Back to all topics →