Composite
Treating a tree of parts and wholes uniformly.
01The intuition
The problem it solves
A file explorer needs the total size of a folder. Folders contain files and other folders. The first version handles two cases by hand and goes one level deep.
def folder_size(folder):
total = 0
for item in folder.items:
if isinstance(item, File):
total += item.bytes
elif isinstance(item, Folder):
for sub in item.items:
if isinstance(sub, File):
total += sub.bytes
# and folders inside folders inside folders...
return total
The code checks types everywhere and still cannot handle a folder three levels deep. Every operation over the tree, size, search, print, permissions, repeats the same type-checking walk. The structure is a tree where a branch and a leaf are different kinds of thing, and the caller is forced to care.
The composite pattern gives leaves and branches the same interface, so a caller can ask a single item, a folder, or the whole tree the same question and get the right answer. The branch answers by asking its children.
Build it one piece at a time
Step 1: name the shared interface. Everything in the tree is a Node that can report its size and its name.
from abc import ABC, abstractmethod
class Node(ABC):
def __init__(self, name: str): self.name = name
@abstractmethod
def size(self) -> int: ...
Step 2: the leaf answers directly.
class File(Node):
def __init__(self, name: str, bytes: int):
super().__init__(name); self.bytes = bytes
def size(self) -> int:
return self.bytes
Step 3: the branch answers by delegating to its children. It does not know or care whether a child is a file or another folder. Recursion falls out for free.
class Folder(Node):
def __init__(self, name: str):
super().__init__(name); self.children: list[Node] = []
def add(self, node: Node) -> "Folder":
self.children.append(node); return self
def size(self) -> int:
return sum(child.size() for child in self.children)
root = Folder("home").add(File("notes.txt", 300)).add(
Folder("photos").add(File("a.jpg", 2000)).add(Folder("old").add(File("b.jpg", 500))))
print(root.size()) # 2800, any depth
Step 4: add operations the same way. Printing the tree, counting files or finding a path are each one method on Node, with the leaf doing the simple thing and the folder looping.
class File(Node):
def tree(self, depth: int = 0) -> str:
return " " * depth + self.name + "\n"
class Folder(Node):
def tree(self, depth: int = 0) -> str:
return " " * depth + self.name + "/\n" + "".join(c.tree(depth + 1) for c in self.children)
| Step | isinstance checks in callers | Depth supported | Adding "count files" costs |
|---|---|---|---|
| Start | one per level per operation | 2 | another nested walk |
| 1 to 3 | 0 | any | one method on each of 2 classes |
Why it works
The composite makes the tree self-similar from the caller's point of view: a folder of files behaves like a file, so the same call works at the root, at a leaf, or anywhere between. The invariant is "every node, leaf or branch, honours the full Node interface". Recursion is not something the caller writes; it is what happens when a branch delegates. This is polymorphism applied to a part-whole structure.
Where it breaks: operations that make sense only for one kind. If Node gains add(child), a File must raise, which violates Liskov substitution; keep child management on Folder only. Cycles are the other trap: a folder that contains an ancestor makes size loop forever, so a tree that can be built from untrusted input needs a visited set or a parent check.
The trade-off you must say out loud
Composite makes the structure uniform at the cost of making leaves and branches look more alike than they are, so a reader cannot tell from a type whether a node has children. It is the right tool for recursive structures with operations that aggregate over the whole; it is the wrong tool for a flat list with two item kinds. Say the recursion depth you expect and whether you need an iterative walk to avoid stack limits. The follow-up is often "how would you add an operation without touching every class", and the answer is the Visitor.
Where it shows up in the 100 Google problems
- Design an in-memory file system: directories and files share
Node;ls,sizeand path resolution are tree walks. - Compute sizes and validate a hierarchical file system: the size method above plus cycle detection and hard-link handling is the whole question.
- Design a boolean and arithmetic expression library: constants are leaves, operators are branches, and
evaluaterecurses the same waysizedoes. - Design a JSON parser: objects and arrays are composites of values; serialisation is the
treemethod. - Design a role and permission system: roles contain permissions and other roles; "has permission" is a composite query with a visited set against cycles.
02Used in these problems
- Design a flattening iterator over nested lists
- Design a JSON parser
- Design a boolean and arithmetic expression library
- Merge two configuration trees with per-node rules
- Design an in-memory file system
- Design a path-value file system API
- Compute sizes and validate a hierarchical file system
- Design a role and permission system