2.4 Composite
The Composite pattern lets you organize objects into a tree and treat "a single object" and "a composition of objects" uniformly. A file system is the classic example: files are leaves, folders are containers, but when you ask "how much space does this use," you want to ask files and folders through the same method.
The core: leaves and containers implement the same interface. The client holds a node and never needs to check whether it is a file or a folder — it just calls size(), and recursion unfolds the whole subtree.
In the file tree below, click a folder to expand/collapse; the size on the right of each node is computed recursively as "the sum of children's sizes."
A uniform interface is the key
interface Node {
int size();
}
class File implements Node {
public int size() { return bytes; } // leaf: return directly
}
class Folder implements Node {
private List<Node> children = new ArrayList<>();
public int size() {
return children.stream().mapToInt(Node::size).sum(); // container: recursive sum
}
}Folder.size() forwards the request to each child, and a child may itself be a Folder — recursion forms naturally.
More than file trees
Any "part-whole" hierarchy fits: salary roll-ups in an org chart, nested UI layouts, combos and items in an order, menus and submenus. The budget lab below lets you adjust each team's headcount (leaves) and watch the CEO node's total recompute bottom-up.
A design tradeoff: transparency vs safety
Composite has a classic debate — where do add(child) and remove(child) go?
- Transparent: put
add/removeon the sharedNodeinterface. The client treats leaves and containers identically, but callingadd()on a leaf is meaningless (it can only throw or be a no-op).
- Safe: declare
add/removeonly onFolder. The semantics are safer, but the client sometimes must check the type.
There is no single right answer — it depends on whether you value uniformity or type safety more.