Design Patterns · Structural Patterns
Composite
Give leaves and containers one meaningful contract. Let containers delegate recursively through an object tree.
This lesson follows Bridge. It uses composition too, but organizes objects into a recursive tree.
Composite lets a client treat one object and a group of objects through the same interface. A container delegates to its children, which can be leaves or more containers. The cost is finding an operation that makes sense for both.
Start from a real tree
An order box contains a book and a smaller accessory box. The small box contains a cable and a plug. A client wants the price. It should not need an instanceof branch at every depth to distinguish a product from a box.
interface PricedItem { price(): number }
class Product implements PricedItem {
constructor(private amount: number) {}
price(): number { return this.amount; }
}
class Box implements PricedItem {
private children: PricedItem[] = [];
add(item: PricedItem) { this.children.push(item); }
price(): number {
return this.children.reduce((sum, item) => sum + item.price(), 0);
}
}
const accessories = new Box();
accessories.add(new Product(10));
accessories.add(new Product(5));
const order = new Box();
order.add(new Product(20));
order.add(accessories);
order.price(); // 35
Follow the recursive work
Step through
Calculate the order total
Call the root
order.price()
sum starts at 0
Read the first leaf
book.price() → 20
root subtotal → 20
Enter the nested container
accessories.price() delegates to its children
Return child results
cable → 10; plug → 5
accessory box → 15
order → 20 + 15 = 35
The operation need not be a sum. A drawing group calls draw() on each child. A menu container can search its descendants. The common operation and combination rule define the design.
Put child management where it is honest
Try it
Should every component expose add()?
PricedItem: price()
Box: add(), price()
Product: price()
Leaves do not promise child management. Code that builds the tree needs to know which objects are containers.
PricedItem: add(), price()
Product.add() → throws or does nothing
The API looks uniform, but a leaf cannot honor the same operation. State that trade-off. The narrower interface is often clearer.
A tree must stay a tree
Reject insertion of an ancestor as a descendant. Otherwise recursive price() can loop forever. If the same item appears under two parents, decide whether that shared node represents two charges or one. The tiny example assumes an acyclic tree and visits each child occurrence.
Map the roles
| Role | In this example | Job |
|---|---|---|
| Component | PricedItem | Declares the operations that make sense for both simple and complex elements |
| Leaf | Product | A basic element with no children. Leaves usually do most of the real work, because they cannot delegate it. |
| Container (composite) | Box | Holds leaves or other containers, knows them only through the component interface, delegates to them, and combines their results |
| Client | the order total code | Works with every element through the component interface, so simple and complex elements look the same |
Check yourself
Which role usually does most of the real work in a composite tree?
Correct. Containers mostly delegate and combine. Leaves compute their own values.
Not quite. The root delegates like every other container.
Reach for it when your model is a tree
Composite is worth it only when the core of your model really is a tree.
- you need a tree-like object structure. Composite gives you two element types that share one interface: simple leaves and containers. Containers can hold leaves and other containers, so the structure nests to any depth.
- client code should treat simple and complex elements the same way. Every element shares the interface, so the client never needs to know the concrete class it is calling.
Check yourself
A checkout has a flat list of line items and no nesting. Does Composite help?
Not quite. A flat collection gains nothing from container and leaf roles.
Correct. Composite earns its cost from recursive nesting.
Implement it in six steps
Refactor existing code toward the pattern in this order:
- Make sure the core model can be represented as a tree. Break it into simple elements and containers, and remember that a container must accept both.
- Declare the component interface with methods that make sense for both simple and complex elements.
- Create a leaf class for simple elements. A program can have several leaf classes.
- Create a container class with an array field for its children. Type the array with the component interface so it can hold leaves and containers.
- Implement the component methods in the container by delegating most of the work to its children.
- Add methods to add and remove children. Put them on the container, or on the component interface. On the interface, leaves get empty methods, which breaks interface segregation, but clients can then treat every element the same, even when building the tree.
Check yourself
Why is the container's children array typed as PricedItem[] rather than Product[]?
Correct. Typing it as the leaf class would make nesting impossible.
Not quite. They can. The point is which instances it accepts.
Name what it costs
| You gain | You pay |
|---|---|
| Work with complex trees through polymorphism and recursion | A common interface is hard to define for classes whose behavior differs too much |
| New element types join the tree without changing existing code (open/closed) | Forcing one interface can overgeneralize it until it is hard to understand |
| One client operation for leaves and groups | Cycles, ownership, and depth need explicit rules |
Check yourself
A tree needs price() on boxes and play() only on audio items. What does that tell you about the shared interface?
Not quite. Most elements would then carry methods they cannot honor.
Correct. Forcing unrelated behavior into one interface is the overgeneralization the pattern warns about.
Do not confuse it with its neighbors
| Pattern | How it relates |
|---|---|
| Decorator | Both rely on recursive composition, so their diagrams look alike. A decorator has one child and adds responsibilities to it. A composite has many children and combines their results. A decorator can extend one object inside a composite tree. |
| Builder | Use a builder to assemble complex composite trees, because its steps can run recursively. |
| Chain of Responsibility | A leaf can pass a request up through its parent containers to the root. The tree supplies the chain. |
| Iterator | Use an iterator to traverse a composite tree. |
| Visitor | Use a visitor to run an operation over an entire composite tree. |
| Flyweight | Shared leaf nodes can be flyweights to save memory. |
| Prototype | Designs heavy in Composite benefit from cloning a finished structure instead of rebuilding it. |
Check yourself
A help request starts at a button and climbs to its panel and then the window until one can answer. Which pattern does the tree make easy?
Correct. The parent links in the tree form a ready-made chain of handlers.
Not quite. Flyweight shares memory. It does not route requests.
Retrieve and apply
Check yourself
A folder has files and subfolders. Which interface is the strongest common starting point?
Correct. A file reports its own bytes. A folder can sum child sizes. Both keep the same promise.
Not quite. A file cannot contain children. Putting that promise on every node forces unsupported behavior.
Draw a nested menu and state how find(label) combines child results. Continue to Decorator to stack behavior around a single component.
Source: Alexander Shvets, Dive Into Design Patterns (深入设计模式), Chinese edition v2021-1.25. Composite, printed pages 169–180. Explanations, examples, and exercises are adapted for this course.