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.

Watch it evolve1 / 9
The problem

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.

80
Start
Stage 2
Final

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.

The first versionJava
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());
    }
}
***Item− name: String− cents: int~ name(): String~ priceCents(): intBundle− name: String− percentOff: int− items: List<Item>~ name(): String~ add(Item)~ priceCents(): int~ itemCount(): intOrder− items: List<Item>− bundles: List<Bundle>~ add(Item)~ add(Bundle)~ totalCents(): int~ itemCount(): int

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:

Bundles inside bundles, the direct wayJava
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.
  • Order and Bundle do 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.

Java
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;
    }
}
*«interface»Offering+ name(): String+ priceCents(): int+ itemCount(): intItem− name: String− cents: int+ name(): String+ priceCents(): int+ itemCount(): intBundle− name: String− percentOff: int− parts: List<Offering>+ name(): String~ add(Offering)+ priceCents(): int+ itemCount(): int

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
Two kinds, one level
4
Two kinds, nested
6
One interface
2

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.

*«interface»Offering+ name(): String+ priceCents(): int+ itemCount(): intItem− name: String− cents: int+ name(): String+ priceCents(): int+ itemCount(): intBundle− name: String− percentOff: int− parts: List<Offering>+ name(): String~ add(Offering)+ priceCents(): int+ itemCount(): int

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

Offering

The interface every node implements, leaf or group

Leaf

Item

A node with no children, which answers on its own

Composite

Bundle

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 add() is declared

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 add(), which breaks Liskov substitution

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 List of the interface type; the leaf and composite both implement it

C++

A vector of std::unique_ptr to the component, so the composite owns its children and they are destroyed with it

JavaScript

An array of any objects with the right methods; nothing checks that a part fits

Python

A list typed with a Protocol; a type checker checks parts, the run time doesn't

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

1. The bakery adds gift cards: a line with a price but nothing for the kitchen. What changes?
2. Why does Bundle.add() take an Offering rather than an Item?
3. A conference package holds three team breakfasts. What has to change to price it?
4. Where should add() be declared?

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.