Design Patterns, Explained Simply · Structural Patterns: Fitting Objects Together
Composite
Treat a single object and a group of objects the same way, so code can work with whole trees, like folders, menus or org charts, through one interface.
The problem
A file browser shows the size of each folder. A folder contains files and other folders, nested to any depth. The first version has code like if isinstance(item, File): ... elif isinstance(item, Folder): for child in item.children: ..., and the same branching appears in size calculation, search, rendering and permission checks.
Every new kind of item (a symlink, a zip archive treated as a folder) means updating all those if chains.
Real-world analogy
An army's orders: a general gives an order to a division, the division passes it to its brigades, they pass it to companies, down to individual soldiers. The general doesn't care whether a unit is one soldier or ten thousand; they just say "advance", and each unit handles it, passing it on to its members if it has any.
Without the pattern
def total_size(item):
if isinstance(item, File):
return item.size
elif isinstance(item, Folder):
return sum(total_size(child) for child in item.children)
# repeated in search(), render(), can_read()...
The pattern
- Component interface: operations common to leaves and containers (
size(),render()). - Leaf (
File): a simple element that does the work itself. - Composite (
Folder): holds children (leaves or other composites) and implements each operation by delegating to its children and combining the results. - Client: calls
size()on any component, a single file or a whole tree, without checking which it is.
The code
class File: # leaf
def __init__(self, name, size):
self.name, self._size = name, size
def size(self):
return self._size
def render(self, indent=0):
return [" " * indent + f"{self.name} ({self._size} KB)"]
class Folder: # composite: contains files and folders
def __init__(self, name, children):
self.name, self.children = name, children
def size(self):
return sum(child.size() for child in self.children) # delegate and combine
def render(self, indent=0):
lines = [" " * indent + f"{self.name}/ ({self.size()} KB)"]
for child in self.children:
lines += child.render(indent + 2)
return lines
project = Folder("project", [
File("README.md", 4),
Folder("src", [File("app.py", 12), File("utils.py", 6), Folder("assets", [File("logo.png", 40)])]),
Folder("docs", []),
])
print("\n".join(project.render()))
print("one file is a component too:", File("notes.txt", 2).size(), "KB")
Output
project/ (62 KB)
README.md (4 KB)
src/ (58 KB)
app.py (12 KB)
utils.py (6 KB)
assets/ (40 KB)
logo.png (40 KB)
docs/ (0 KB)
one file is a component too: 2 KBinterface Node {
size(): number;
render(indent?: number): string[];
}
class File implements Node { // leaf
constructor(private name: string, private bytes: number) {}
size() { return this.bytes; }
render(indent = 0) { return [" ".repeat(indent) + `${this.name} (${this.bytes} KB)`]; }
}
class Folder implements Node { // composite: contains files and folders
constructor(private name: string, private children: Node[]) {}
size(): number {
return this.children.reduce((sum, child) => sum + child.size(), 0); // delegate and combine
}
render(indent = 0): string[] {
const lines = [" ".repeat(indent) + `${this.name}/ (${this.size()} KB)`];
for (const child of this.children) lines.push(...child.render(indent + 2));
return lines;
}
}
const project = new Folder("project", [
new File("README.md", 4),
new Folder("src", [new File("app.py", 12), new File("utils.py", 6), new Folder("assets", [new File("logo.png", 40)])]),
new Folder("docs", []),
]);
console.log(project.render().join("\n"));
console.log("one file is a component too:", new File("notes.txt", 2).size(), "KB");
Output
project/ (62 KB)
README.md (4 KB)
src/ (58 KB)
app.py (12 KB)
utils.py (6 KB)
assets/ (40 KB)
logo.png (40 KB)
docs/ (0 KB)
one file is a component too: 2 KBimport java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Node project = new Folder("project", List.of(
new File("README.md", 4),
new Folder("src", List.of(new File("app.py", 12), new File("utils.py", 6),
new Folder("assets", List.of(new File("logo.png", 40))))),
new Folder("docs", List.of())));
System.out.println(String.join("\n", project.render(0)));
System.out.println("one file is a component too: " + new File("notes.txt", 2).size() + " KB");
}
}
interface Node {
int size();
List<String> render(int indent);
}
record File(String name, int kb) implements Node { // leaf
public int size() { return kb; }
public List<String> render(int indent) { return List.of(" ".repeat(indent) + name + " (" + kb + " KB)"); }
}
record Folder(String name, List<Node> children) implements Node { // composite: contains files and folders
public int size() {
return children.stream().mapToInt(Node::size).sum(); // delegate and combine
}
public List<String> render(int indent) {
List<String> lines = new ArrayList<>(List.of(" ".repeat(indent) + name + "/ (" + size() + " KB)"));
for (Node child : children) lines.addAll(child.render(indent + 2));
return lines;
}
}
Output
project/ (62 KB)
README.md (4 KB)
src/ (58 KB)
app.py (12 KB)
utils.py (6 KB)
assets/ (40 KB)
logo.png (40 KB)
docs/ (0 KB)
one file is a component too: 2 KBRun this on your computer — see “Get Started” for setup.
<?php
interface Node {
public function size(): int;
public function render(int $indent = 0): array;
}
class File implements Node { // leaf
public function __construct(private string $name, private int $kb) {}
public function size(): int { return $this->kb; }
public function render(int $indent = 0): array {
return [str_repeat(" ", $indent) . "{$this->name} ({$this->kb} KB)"];
}
}
class Folder implements Node { // composite: contains files and folders
/** @param Node[] $children */
public function __construct(private string $name, private array $children) {}
public function size(): int {
return array_sum(array_map(fn(Node $c) => $c->size(), $this->children)); // delegate and combine
}
public function render(int $indent = 0): array {
$lines = [str_repeat(" ", $indent) . "{$this->name}/ ({$this->size()} KB)"];
foreach ($this->children as $child) array_push($lines, ...$child->render($indent + 2));
return $lines;
}
}
$project = new Folder("project", [
new File("README.md", 4),
new Folder("src", [new File("app.py", 12), new File("utils.py", 6), new Folder("assets", [new File("logo.png", 40)])]),
new Folder("docs", []),
]);
echo implode("\n", $project->render()), "\n";
echo "one file is a component too: ", (new File("notes.txt", 2))->size(), " KB\n";
Output
project/ (62 KB)
README.md (4 KB)
src/ (58 KB)
app.py (12 KB)
utils.py (6 KB)
assets/ (40 KB)
logo.png (40 KB)
docs/ (0 KB)
one file is a component too: 2 KBproject.size() walks the whole tree without a single type check. A new item type, such as a symlink, only needs to implement size() and render().
When to use it
- Your data is a tree: folders and files, menus and menu items, UI containers and widgets, org charts, product bundles, expression trees.
- Clients should treat one item and a group the same way ("total price" of one product or a bundle of bundles).
When not to use it
- The structure is flat. A list is enough.
- Leaves and containers really need different operations. Forcing them into one interface leads to leaves with methods like
add_child()that throw errors (a Liskov smell). - Very deep or huge trees: recursion depth and repeated recomputation (like
size()called at every level) may need caching.
You've already used it
- The DOM: every
ElementandTextnode is aNode; elements contain other nodes, and operations liketextContentwork on any subtree. - React: components render other components, and the whole UI is one tree of the same kind of thing.
- Java Swing / AWT:
Containeris aComponentthat holds other components.
Key points
- Composite lets clients treat single objects and groups uniformly through one interface.
- Containers implement operations by delegating to children and combining results.
- It removes
if leaf / if containerbranching from client code. - Further reading: Refactoring Guru: Composite.
Practice questions
What problem does the Composite pattern solve?
Answer
It lets client code treat individual objects and groups of objects (trees) the same way through one interface. Containers implement each operation by delegating to their children, so clients never need if leaf / else container checks.
Name three real-world structures that suit the Composite pattern.
Answer
File systems (folders and files), UI component trees (the DOM, React, Swing containers), menus with submenus, org charts and product bundles that contain products or other bundles.
What is a common design difficulty with Composite?
Answer
Deciding where child-management methods (add, remove) go. Putting them on the shared interface makes leaves implement methods that make no sense (a Liskov smell); putting them only on composites means clients must sometimes know the concrete type.
Open in the app to mark lessons complete, take notes and track your progress.