Composite
Composite is a structural pattern that lets individual objects and groups of objects be treated through the same interface, so client code can operate on a whole tree without caring how deep it goes.
Problem
A file manager needs to compute the total size of anything a user selects: a single file, or a directory that itself contains files and other directories nested several levels deep. The obvious first attempt writes a function that checks if (item.type === 'file') versus if (item.type === 'directory') and sums up children in the directory branch.
That branch multiplies everywhere size is computed - disk-usage reports, copy previews, quota checks - and every one of them has to be kept in sync. Add a third kind of filesystem entry, like a symlink that points at a directory, and every one of those call sites needs a new branch.
Solution
Give every filesystem entry - files and directories alike - the same interface, say a getSize() method. A file simply returns its own size. A directory implements getSize() by looping over whatever it contains and summing each child's getSize(), regardless of whether that child is itself a file or another directory.
Because a directory calls the exact same method it exposes, the recursion handles arbitrary nesting for free. Any code that needs a size just calls getSize() on whatever it was handed, without ever branching on what kind of item it is.
When to Use
- Your domain is naturally a part-whole hierarchy - folders and files, menus and menu items, org charts, nested UI layouts.
- You want callers to run the same operation on a single item or an entire subtree without writing separate code paths for each case.
Real-World Examples
- Filesystem APIs - a
Fileand aDirectoryboth expose size and traversal operations; computing a folder's total size just recurses through whatever it contains. - DOM tree - a text node and an element node both implement the same
Nodeinterface, so traversal code likequerySelectorAllworks uniformly at any depth. - Protobuf / JSON AST parsers - a scalar value and a nested object both conform to one node interface, letting a single visitor function walk the entire parsed tree.
Structure
A single shared interface covers both ends of the tree. Leaf nodes implement it by computing their own answer directly; container nodes implement the very same interface by looping over their children and combining whatever those children return - including other containers.
Participants
The shared interface that both single elements and containers implement, defining the operations client code is allowed to call.
In the diagram: Component.
A terminal node with no children of its own; it answers a Component call directly instead of forwarding it anywhere.
In the diagram: File A, File B, File C.
A node that keeps a collection of child Components - leaves, other composites, or a mix - and answers a Component call by combining the results of calling it on each child.
In the diagram: Directory.
Calls Component operations on whatever object it's given, never checking whether it's holding a single element or an entire subtree.
In the diagram: Client.
class FileSystemItem {
getName() { throw new Error('Not implemented'); }
getSize() { throw new Error('Not implemented'); }
print(indent = '') { throw new Error('Not implemented'); }
}
class File extends FileSystemItem {
constructor(name, size) {
super();
this.name = name;
this.size = size;
}
getName() { return this.name; }
getSize() { return this.size; }
print(indent = '') {
console.log(`${indent}[file] ${this.name} (${this.size} KB)`);
}
}
class Directory extends FileSystemItem {
#children = [];
constructor(name) {
super();
this.name = name;
}
add(item) { this.#children.push(item); return this; }
remove(item) { this.#children = this.#children.filter(c => c !== item); }
getName() { return this.name; }
getSize() { return this.#children.reduce((sum, c) => sum + c.getSize(), 0); }
print(indent = '') {
console.log(`${indent}[dir] ${this.name}/`);
this.#children.forEach(c => c.print(indent + ' '));
}
}
const root = new Directory('project');
const src = new Directory('src');
const tests = new Directory('tests');
src.add(new File('index.js', 12)).add(new File('app.js', 34));
tests.add(new File('app.test.js', 8));
root.add(src).add(tests).add(new File('package.json', 2));
root.print();
console.log(`Total size: ${root.getSize()} KB`);
Step 1 of 6
FileSystemItem is the shared component interface
`getName`, `getSize`, `print` are declared with no logic - both leaf files and composite directories must implement the same three methods.
Step 2 of 6
File is a leaf - it has no children
`getSize()` just returns `this.size` directly, and `print()` logs one line - a leaf handles the request itself, with nothing to delegate to.
Step 3 of 6
Directory is a composite that stores children
The private `#children` array holds any mix of `File` or `Directory` items, and `add()`/`remove()` manage that collection - a directory can contain other directories.
Step 4 of 6
getSize() delegates to children and sums the results
`this.#children.reduce((sum, c) => sum + c.getSize(), 0)` calls `getSize()` on every child regardless of whether it's a `File` or another `Directory` - the composite doesn't need to know which.
Step 5 of 6
print() recurses through the whole subtree
After logging its own line, `Directory.print()` calls `c.print(indent + ' ')` on every child - each child prints itself (and its own children, if any) with deeper indentation.
Step 6 of 6
Client code treats files and directories uniformly
`root.add(src).add(tests).add(new File(...))` mixes `Directory` and `File` objects through the same `add()` call, and `root.getSize()` returns the total across the whole nested tree without any type checks.
interface FileSystemItem {
getName(): string;
getSize(): number;
print(indent?: string): void;
}
class File implements FileSystemItem {
constructor(private name: string, private size: number) {}
public getName(): string { return this.name; }
public getSize(): number { return this.size; }
public print(indent: string = ''): void {
console.log(`${indent}[file] ${this.name} (${this.size} KB)`);
}
}
class Directory implements FileSystemItem {
private children: FileSystemItem[] = [];
constructor(private name: string) {}
public add(item: FileSystemItem): this {
this.children.push(item);
return this;
}
public remove(item: FileSystemItem): void {
this.children = this.children.filter(c => c !== item);
}
public getName(): string { return this.name; }
public getSize(): number {
return this.children.reduce((sum, c) => sum + c.getSize(), 0);
}
public print(indent: string = ''): void {
console.log(`${indent}[dir] ${this.name}/`);
this.children.forEach(c => c.print(indent + ' '));
}
}
const root = new Directory('project');
const src = new Directory('src');
const tests = new Directory('tests');
src.add(new File('index.ts', 12)).add(new File('app.ts', 34));
tests.add(new File('app.test.ts', 8));
root.add(src).add(tests).add(new File('package.json', 2));
root.print();
console.log(`Total size: ${root.getSize()} KB`);
import java.util.*;
interface FileSystemItem {
String getName();
int getSize();
void print(String indent);
}
class File implements FileSystemItem {
private final String name;
private final int size;
File(String name, int size) { this.name = name; this.size = size; }
public String getName() { return name; }
public int getSize() { return size; }
public void print(String indent) {
System.out.printf("%s[file] %s (%d KB)%n", indent, name, size);
}
}
class Directory implements FileSystemItem {
private final String name;
private final List<FileSystemItem> children = new ArrayList<>();
Directory(String name) { this.name = name; }
public Directory add(FileSystemItem item) { children.add(item); return this; }
public void remove(FileSystemItem item) { children.remove(item); }
public String getName() { return name; }
public int getSize() { return children.stream().mapToInt(FileSystemItem::getSize).sum(); }
public void print(String indent) {
System.out.printf("%s[dir] %s/%n", indent, name);
children.forEach(c -> c.print(indent + " "));
}
}
Directory root = new Directory("project");
Directory src = new Directory("src");
Directory tests = new Directory("tests");
src.add(new File("index.java", 12)).add(new File("App.java", 34));
tests.add(new File("AppTest.java", 8));
root.add(src).add(tests).add(new File("pom.xml", 2));
root.print("");
System.out.println("Total size: " + root.getSize() + " KB");
interface IFileSystemItem
{
string GetName();
int GetSize();
void Print(string indent = "");
}
class File : IFileSystemItem
{
private readonly string _name;
private readonly int _size;
public File(string name, int size) { _name = name; _size = size; }
public string GetName() => _name;
public int GetSize() => _size;
public void Print(string indent = "") =>
Console.WriteLine($"{indent}[file] {_name} ({_size} KB)");
}
class Directory : IFileSystemItem
{
private readonly string _name;
private readonly List<IFileSystemItem> _children = new();
public Directory(string name) => _name = name;
public Directory Add(IFileSystemItem item) { _children.Add(item); return this; }
public void Remove(IFileSystemItem item) => _children.Remove(item);
public string GetName() => _name;
public int GetSize() => _children.Sum(c => c.GetSize());
public void Print(string indent = "")
{
Console.WriteLine($"{indent}[dir] {_name}/");
foreach (var child in _children) child.Print(indent + " ");
}
}
var root = new Directory("project");
var src = new Directory("src");
var tests = new Directory("tests");
src.Add(new File("index.cs", 12)).Add(new File("App.cs", 34));
tests.Add(new File("AppTest.cs", 8));
root.Add(src).Add(tests).Add(new File("project.csproj", 2));
root.Print();
Console.WriteLine($"Total size: {root.GetSize()} KB");
from __future__ import annotations
from abc import ABC, abstractmethod
class FileSystemItem(ABC):
@abstractmethod
def get_name(self) -> str:
pass
@abstractmethod
def get_size(self) -> int:
pass
@abstractmethod
def print(self, indent: str = '') -> None:
pass
class File(FileSystemItem):
def __init__(self, name: str, size: int) -> None:
self._name = name
self._size = size
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return self._size
def print(self, indent: str = '') -> None:
print(f"{indent}[file] {self._name} ({self._size} KB)")
class Directory(FileSystemItem):
def __init__(self, name: str) -> None:
self._name = name
self._children: list[FileSystemItem] = []
def add(self, item: FileSystemItem) -> 'Directory':
self._children.append(item)
return self
def remove(self, item: FileSystemItem) -> None:
self._children.remove(item)
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return sum(c.get_size() for c in self._children)
def print(self, indent: str = '') -> None:
print(f"{indent}[dir] {self._name}/")
for child in self._children:
child.print(indent + ' ')
root = Directory('project')
src = Directory('src')
tests = Directory('tests')
src.add(File('main.py', 12)).add(File('app.py', 34))
tests.add(File('test_app.py', 8))
root.add(src).add(tests).add(File('requirements.txt', 2))
root.print()
print(f"Total size: {root.get_size()} KB")
Step 1 of 6
FileSystemItem is the shared component interface
`get_name`, `get_size`, `print` are all `@abstractmethod` - both `File` and `Directory` must implement the same three methods to be instantiated.
Step 2 of 6
File is a leaf - it has no children
`get_size()` just returns `self._size` directly, and `print()` prints one formatted line - nothing to recurse into.
Step 3 of 6
Directory is a composite that stores children
`self._children: list[FileSystemItem]` can hold any mix of `File` or `Directory` objects, and `add()` appends to it, returning `self` for chaining.
Step 4 of 6
get_size() delegates to children and sums the results
`sum(c.get_size() for c in self._children)` calls `get_size()` on every child, whether it's a `File` or another `Directory` - the composite treats them identically.
Step 5 of 6
print() recurses through the whole subtree
After printing its own line, `Directory.print()` loops over `self._children` and calls `child.print(indent + ' ')` on each - the recursion unwinds the entire nested tree.
Step 6 of 6
Client code treats files and directories uniformly
`root.add(src).add(tests).add(File(...))` mixes `Directory` and `File` through the same `add()` call, and `root.get_size()` sums the entire nested tree with no type checks.
Click "Run" to execute this code in a sandboxed frame and see console output here.
TypeScript runs as plain JavaScript here - type annotations are stripped, not type-checked.
Click "Run" to execute this code in a sandboxed frame and see console output here.
Advantages
- Eliminates type-checking branches for tree traversal entirely - recursion through a shared interface handles any depth automatically.
- New kinds of leaves or containers slot in by implementing the Component interface, with zero changes to code that already traverses the tree.
- Client code shrinks and simplifies, since it always calls one interface regardless of whether it's handed a single object or a whole subtree.
Disadvantages
- Forcing leaves and containers to share one interface can leave leaves with methods that make no sense for them (like managing children).
- It's tempting to put container-only operations like `add`/`remove` on the shared interface, which then forces leaves to implement no-ops or throw.
- Very deep or very large trees can make simple aggregate operations expensive, since a single call may fan out recursively across the whole structure.
Question 1 of 10
Why is `add`/`remove` on the shared Component interface flagged as a risk in the cons?
Correct! It's tempting to put container-only operations like add/remove on the shared interface, which then forces leaves to implement no-ops or throw, since they have no children to manage.
Not quite. It's tempting to put container-only operations like add/remove on the shared interface, which then forces leaves to implement no-ops or throw, since they have no children to manage.
Question 2 of 10
In the file manager problem, what task does the size-computing function need to handle?
Correct! The file manager needs to compute the total size of anything a user selects: a single file, or a directory that itself contains files and other directories nested several levels deep.
Not quite. The file manager needs to compute the total size of anything a user selects: a single file, or a directory that itself contains files and other directories nested several levels deep.
Question 3 of 10
In the DOM tree real-world example, what makes uniform traversal like `querySelectorAll` possible?
Correct! A text node and an element node both implement the same Node interface, so traversal code like querySelectorAll works uniformly at any depth.
Not quite. A text node and an element node both implement the same Node interface, so traversal code like querySelectorAll works uniformly at any depth.
Question 4 of 10
What goes wrong with the naive `if (item.type === 'file')` / `if (item.type === 'directory')` approach to computing sizes?
Correct! The branch multiplies everywhere size is computed - disk-usage reports, copy previews, quota checks - and every call site needs a new branch for a new entry kind.
Not quite. The branch multiplies everywhere size is computed - disk-usage reports, copy previews, quota checks - and every call site needs a new branch for a new entry kind.
Question 5 of 10
According to the Client's role description, how does it interact with the tree?
Correct! The Client calls Component operations on whatever object it's given, never checking whether it's holding a single element or an entire subtree.
Not quite. The Client calls Component operations on whatever object it's given, never checking whether it's holding a single element or an entire subtree.
Question 6 of 10
In the JavaScript implementation, how does `Directory` store its children?
Correct! The private `#children` array holds any mix of `File` or `Directory` items, and `add()`/`remove()` manage that collection - a directory can contain other directories.
Not quite. The private `#children` array holds any mix of `File` or `Directory` items, and `add()`/`remove()` manage that collection - a directory can contain other directories.
Question 7 of 10
In the walkthrough, what does `Directory.print()` do after logging its own line?
Correct! After logging its own line, `Directory.print()` calls `c.print(indent + ' ')` on every child - each child prints itself, and its own children if any, with deeper indentation.
Not quite. After logging its own line, `Directory.print()` calls `c.print(indent + ' ')` on every child - each child prints itself, and its own children if any, with deeper indentation.
Question 8 of 10
What does Composite let client code do?
Correct! Composite lets individual objects and groups of objects be treated through the same interface, so client code can operate on a whole tree without caring how deep it goes.
Not quite. Composite lets individual objects and groups of objects be treated through the same interface, so client code can operate on a whole tree without caring how deep it goes.
Question 9 of 10
In the pattern's structure, what distinguishes a Leaf from a Composite?
Correct! A Leaf is a terminal node with no children of its own that answers a Component call directly, while a Composite keeps a collection of child Components and answers by combining the results of calling it on each child.
Not quite. A Leaf is a terminal node with no children of its own that answers a Component call directly, while a Composite keeps a collection of child Components and answers by combining the results of calling it on each child.
Question 10 of 10
How does a directory's `getSize()` handle arbitrary nesting in the solution?
Correct! Because a directory calls the exact same method it exposes, the recursion handles arbitrary nesting for free.
Not quite. Because a directory calls the exact same method it exposes, the recursion handles arbitrary nesting for free.