Low-level designCore9 min
Composite
Treat a group of objects the way you treat one. A composite implements the same interface as its parts and answers by asking each part, so callers never check whether they hold one item or a whole tree of them.
Items and bundles on an order
An Order keeps items and bundles in two lists. A Bundle holds items and takes a discount off their sum.
1 · The situation
Items and bundles on a catering order
A bakery takes catering orders. An order has single Items, such as a coffee pot, and Bundles: a breakfast box holds a croissant, a muffin and a jug of juice, at 10% off the sum of its items. Every order needs two numbers, the price and how many items the kitchen has to prepare.
The first version has Item, Bundle, which holds a list of items, and Order, which keeps its items and its bundles in two lists. Python writes price_cents() and item_count(). JavaScript and Python have no method overloading, so their Order.add() checks which kind it was given.
import java.util.ArrayList;
import java.util.List;
class Item {
private final String name;
private final int cents;
Item(String name, int cents) {
this.name = name;
this.cents = cents;
}
String name() {
return name;
}
int priceCents() {
return cents;
}
}
class Bundle {
private final String name;
private final int percentOff;
private final List<Item> items = new ArrayList<>();
Bundle(String name, int percentOff) {
this.name = name;
this.percentOff = percentOff;
}
String name() {
return name;
}
void add(Item item) {
items.add(item);
}
int priceCents() {
int sum = 0;
for (Item i : items) sum += i.priceCents();
return sum * (100 - percentOff) / 100;
}
int itemCount() {
return items.size();
}
}
class Order {
private final List<Item> items = new ArrayList<>();
private final List<Bundle> bundles = new ArrayList<>();
void add(Item item) {
items.add(item);
}
void add(Bundle bundle) {
bundles.add(bundle);
}
int totalCents() {
int sum = 0;
for (Item i : items) sum += i.priceCents();
for (Bundle b : bundles) sum += b.priceCents();
return sum;
}
int itemCount() {
int count = items.size();
for (Bundle b : bundles) count += b.itemCount();
return count;
}
}
public class Main {
static String money(int cents) {
return String.format("$%d.%02d", cents / 100, cents % 100);
}
static Bundle breakfastBox() {
Bundle box = new Bundle("Breakfast box", 10);
box.add(new Item("Croissant", 350));
box.add(new Item("Muffin", 300));
box.add(new Item("Juice jug", 900));
return box;
}
public static void main(String[] args) {
Bundle box = breakfastBox();
System.out.println(box.name() + ": " + money(box.priceCents()));
Order order = new Order();
order.add(box);
order.add(breakfastBox());
order.add(new Item("Coffee pot", 1800));
order.add(new Item("Fruit tray", 2400));
System.out.println("Total: " + money(order.totalCents()));
System.out.println("Items for the kitchen: " + order.itemCount());
}
}The class diagram of this version, drawn from its code: an order holds items and bundles in separate lists, and a bundle holds items.
An order of two breakfast boxes, a coffee pot and a fruit tray:
Breakfast box: $13.95 Total: $69.90 Items for the kitchen: 8
2 · The change
A bundle that holds bundles
Offices start ordering a team breakfast: two breakfast boxes, a coffee pot and a fruit tray, at 5% off the whole package. It's a bundle whose contents include bundles. With the current classes, Bundle needs a second list, and every method that walks a bundle needs a second loop:
class Bundle {
private final String name;
private final int percentOff;
private final List<Item> items = new ArrayList<>();
private final List<Bundle> bundles = new ArrayList<>();
Bundle(String name, int percentOff) {
this.name = name;
this.percentOff = percentOff;
}
String name() {
return name;
}
void add(Item item) {
items.add(item);
}
void addBundle(Bundle bundle) {
bundles.add(bundle);
}
int priceCents() {
int sum = 0;
for (Item i : items) sum += i.priceCents();
for (Bundle b : bundles) sum += b.priceCents();
return sum * (100 - percentOff) / 100;
}
int itemCount() {
int count = items.size();
for (Bundle b : bundles) count += b.itemCount();
return count;
}
}Order already had the same two loops, so the shape repeats in both classes and in both operations. The program is right:
Team breakfast: $66.40 Total: $84.40 Items for the kitchen: 9
The cost is in the structure:
- Every operation is written twice in each class: once for items, once for bundles. A third operation, such as prep time, means four more loops.
OrderandBundledo the same thing differently. An order is really a bundle without a discount, but it's a separate class with its own copies of the loops.- A new kind of line multiplies the work. A gift card, with a price but nothing for the kitchen, would need a third list in both classes and a third loop in every method.
The code treats a single item and a group of items as different kinds of thing, so every caller has to ask which one it has. The catering menu is a tree, and the code should let it be one.
3 · The refactor
Let a bundle hold anything that can be ordered
Start from the nested version. Give items and bundles one interface, and let a bundle hold that interface instead of either kind:
Offering declares name(), priceCents() and itemCount(). Item implements it: its price is its own, and it counts as one item.
interface Offering {
String name();
int priceCents();
int itemCount();
}
class Item implements Offering {
private final String name;
private final int cents;
Item(String name, int cents) {
this.name = name;
this.cents = cents;
}
public String name() {
return name;
}
public int priceCents() {
return cents;
}
public int itemCount() {
return 1;
}
}The output is unchanged, and the loops are down to one per operation:
Team breakfast: $66.40 Total: $84.40 Items for the kitchen: 9
Loops over a bundle's or an order's contents, across all classes. The nested version needs two per operation in each class; with one interface, each operation needs one loop in Bundle.
4 · The pattern
Composite: a part and a whole, used alike
Composite arranges objects in a tree and lets callers treat a single object and a group the same way. Every node implements one interface. A leaf answers by itself; a composite holds child nodes and answers by asking each of them, which makes the question travel down the whole tree.
The roles in the bakery's code: Offering is the component, Bundle the composite, and Item the leaf. Bundle keeps its children in its parts field.
Role | In the bakery | Its job |
|---|---|---|
Component |
| The interface every node implements, leaf or group |
Leaf |
| A node with no children, which answers on its own |
Composite |
| A node that holds children and answers by combining theirs |
Client | The code that prices an order | Asks the top node, and never checks what kind it is |
One call at the top travels down the tree; each bundle combines what its parts return. Only one branch is drawn.
The client's question is the same at every depth. That's what makes adding a level free: a conference package that holds three team breakfasts is built from existing classes, and nothing changes to price it.
5 · What it costs
A uniform interface has to fit every node
- The interface must make sense for every node.
priceCents()fits both items and bundles. An operation that only fits groups, such as listing the parts, either goes on the composite alone or has to mean something for a leaf too. - The structure is harder to restrict. Any offering can go in any bundle. A rule such as "a box may only hold food" isn't expressed by the types; it needs a check when parts are added.
- Deep trees cost calls. Each question walks the whole tree. For a catering order that's nothing; for a tree of thousands of nodes asked often, cache the totals or keep them up to date as parts change.
Where | What callers gain | What it costs |
|---|---|---|
On the composite only, as here | Only bundles can be added to, and the compiler checks it | Code that builds the tree must know which nodes are bundles |
On the component interface | Every node can be treated the same, even when building | A leaf must refuse |
6 · Getting it right
Keep the question the same at every level
- Put only shared questions on the component. Price and item count make sense for every node. Questions only a group can answer stay on the composite.
- Combine, don't inspect. A bundle adds up whatever its parts return. If it asks "is this part an item?", the uniform interface is lost.
- Watch for cycles. A bundle that ends up inside itself loops forever. Trees built from code rarely do this; trees built from user input should check.
- Decide who owns the children. Here a bundle owns its parts (in C++, through
std::unique_ptr). If parts are shared between bundles, the tree is really a graph, and totals can count a part twice.
Language | Holding the children |
|---|---|
Java | A |
C++ | A vector of |
JavaScript | An array of any objects with the right methods; nothing checks that a part fits |
Python | A list typed with a |
7 · Look-alikes
Trees, wrappers and chains
Composite holds objects of its own interface, which is also true of several other patterns. What differs is how many it holds and what it does with them:
Pattern | Holds | Does with them |
|---|---|---|
Composite | Many children of its own interface | Combines their answers into its own |
Decorator | One object of its own interface | Adds behavior around the one call |
Chain of Responsibility | One next handler | Passes the request on only if it doesn't handle it |
Iterator | A position in a collection | Hands out the elements one at a time |
Visitor | Nothing; it is passed to each node | Adds a new operation over a tree without editing the node classes |
Composite and Visitor often appear together: the composite gives a tree, and a visitor lets you add operations over it, such as an allergen report, without adding a method to every node class.
8 · In practice
Composites you have already used
User interface toolkits are built this way. In Java's AWT, Container extends Component: a container is a component that can hold other components, so a window holds panels that hold buttons, and laying out or drawing the window walks the tree.
The practice checks recognize a Composite when an interface has an implementation that holds a collection of that same interface and, in one of the interface's methods, calls the same method on each child. A class that holds a list of the interface but never calls through it is reported only as a hint, since it's a container rather than a composite.
Check yourself
4 questions
Takeaways
Remember this
- A Composite lets callers treat one object and a group of objects the same way.
- Leaves and composites implement one interface; a composite holds that interface, not a concrete type.
- A composite answers by asking each child and combining the answers, so the question travels down the tree.
- Adding a level, or a new kind of node, needs no change to the code that walks the tree.
- Put only questions that fit every node on the component.