Composite
In short: Composite gives a single item and a group the same shape, so a tree of products and boxes is handled with one call, however many levels deep it goes.
Problem
A box in a box in a box
The shop sells gift boxes. The holiday box holds tea, honey and a smaller box with candy and chocolate. Every box also has a price for its packaging. The order has a candle and the holiday box. The data looks like this:
record Product(String name, double price) {}
record Box(String name, double packaging, List<Object> items) {}The total is computed by checking what each item is: a product or a box. For a box there is a second loop:
static double total(List<Object> order) { double sum = 0; for (Object item : order) { if (item instanceof Product p) { sum += p.price(); } else if (item instanceof Box box) { sum += box.packaging(); for (Object inner : box.items()) { if (inner instanceof Product q) { sum += q.price(); } } } } return sum;}List<Object> order = List.of( new Product("Candle", 5.00), new Box("Holiday box", 2.00, List.of( new Product("Tea", 4.50), new Product("Honey", 6.00), new Box("Small box", 1.00, List.of( new Product("Candy", 3.50), new Product("Chocolate", 2.50))))));System.out.printf("Total: €%.2f%n", total(order));Total: €17.50The order costs €24.50, and the program says €17.50. The small box is on the third level. The second loop sees it, but it isn't a product, and it disappears from the total together with the candy and the chocolate. There is no error.
A third nested loop fixes this order, but not a box on the fourth level. And the same chain of checks is in counting the products and in the weight for the courier too.
The idea
Composite: the part and the whole with one shape
The product and the box are asked the same thing: how much are you? The product answers with its price. The box answers with its packaging plus the answers of everything inside.
So both have one shape: the interfaceItem (IItem in C#) with a method total(). The box keeps a list of Item and doesn't know which of them is a product and which is a box.This is the Composite pattern. It has three roles:
- the common shape: the interface with
total(); - the leaf: the product, which computes on its own;
- the container: the box, which keeps children with the same shape and asks each of them.
A box in the box is just a child that also asks its children. This way the calls go down to the products, however many levels there are: this is recursion through objects. The code that asks for the total doesn't know whether it holds a product, a box or a whole tree.
A folder with files has the same shape: the size of a file is its own, and the size of a folder is the total of everything in it, including the folders inside.
See it
The total goes down to the bottom and comes back
The common shape:
interface Item { double total();}The product and the box. The constructor of the box takes any number of items (Item... items):
record Product(String name, double price) implements Item { @Override public double total() { return price; }}
class Box implements Item { private final String name; private final double packaging; private final List<Item> items;
public Box(String name, double packaging, Item... items) { this.name = name; this.packaging = packaging; this.items = List.of(items); }
@Override public double total() { double sum = packaging; for (Item item : items) { sum += item.total(); } return sum; }}The tree is written as nested constructors, one item per line:
Item order = new Box("Order", 0, new Product("Candle", 5.00), new Box("Holiday box", 2.00, new Product("Tea", 4.50), new Product("Honey", 6.00), new Box("Small box", 1.00, new Product("Candy", 3.50), new Product("Chocolate", 2.50))));System.out.printf("Total: €%.2f%n", order.total());Total: €24.50Item order = new Box("Order", 0, new Product("Candle", 5.00), new Box("Holiday box", 2.00, new Product("Tea", 4.50), new Product("Honey", 6.00), new Box("Small box", 1.00, new Product("Candy", 3.50), new Product("Chocolate", 2.50))));System.out.printf("Total: €%.2f%n", order.total());Step by step, in words
- The tree is put together: every item is on its own line, and every box holds its children.
- total() asks the root. The order doesn't know what is inside: it starts from its packaging and calls total() on every child.
- Candle is a product: it answers with its price, 5.00, and the answer goes back up.
- Holiday box is a box: it does exactly the same, packaging first, then one child at a time.
- Small box is on the third level. Nothing disappears here: it asks its own children.
- Small box: 1.00 + 3.50 + 2.50 = 7.00. Its total goes up to its parent.
- Holiday box: 2.00 + 4.50 + 6.00 + 7.00 = 19.50.
- Order: 0.00 + 5.00 + 19.50 = 24.50. That is what the program prints.
There is no type check anywhere and no number of levels anywhere. The box does the same thing on every level: the packaging plus the total of the children.
See it
A new kind of item without touching the box
The shop starts selling coffee beans at €24 per kilo. Its price is the price per kilo times the kilos. This is a new class with the same shape:
record WeighedProduct(String name, double perKg, double kg) implements Item { @Override public double total() { return perKg * kg; }}Item coffee = new Box("Coffee box", 1.00, new WeighedProduct("Coffee beans", 24.00, 0.25), new Product("Mug", 8.00));System.out.printf("Coffee box: €%.2f%n", coffee.total());Coffee box: €15.00Not a single line of Box changed. The box calls total() on the coffee, as on any other item.
In the problem the list was a List<Object> and took anything. Now the box takes only an Item, and text in place of a product doesn't compile:
Item box = new Box("Box", 1.00, "Tea");error: constructor Box in class Box cannot be applied to given types; required: String,double,Item[] found: String,double,String reason: varargs mismatch; String cannot be converted to ItemMore precisely, if you are curious
A new kind of item is easy: one class. A new operation costs more: if besides the total you also need a weight for the courier, the method weight() is added to every class. Composite is strong where the kinds of items grow and the operations are few.
Try it
Predict the output
The box holds an empty box and a candle. What does the program print?
Item box = new Box("Box", 1.00, new Box("Empty", 0.50), new Product("Candle", 5.00));System.out.printf("Box: €%.2f%n", box.total());Try it
Find the bug
Someone rewrites the box to make it "safer":
class Box implements Item { private final String name; private final double packaging; private final List<Item> items;
public Box(String name, double packaging, Item... items) { this.name = name; this.packaging = packaging; this.items = List.of(items); }
@Override public double total() { double sum = packaging; for (Item item : items) { if (item instanceof Product) { sum += item.total(); } } return sum; }}Item gift = new Box("Holiday box", 2.00, new Product("Tea", 4.50), new Product("Honey", 6.00), new Box("Small box", 1.00, new Product("Candy", 3.50), new Product("Chocolate", 2.50)));System.out.printf("Holiday box: €%.2f%n", gift.total());Holiday box: €12.50The holiday box costs €19.50, and the program says €12.50. Which line is the bug on?
Pick the line with the bug.
The check lets only products through. The small box inside isn't a product, so it, the candy and the chocolate (€7) disappear from the total. This is the same bug as in the problem, only hidden inside the box. The container in Composite doesn't ask what the child is: it calls total() on every one. Remove the check, and the total is €19.50.
The idea
When you don't need it
Composite has a cost: a common shape and recursion that you have to read to understand one total. It isn't worth it when:
- the things are in a flat list. A cart without boxes is a list, and the total is one loop;
- there are exactly two levels and it stays that way: an order with lines. An order line doesn't contain an order;
- the operations become many, and there are two kinds of items. Every new operation is written in every class. Then it is sometimes clearer for one function to walk the tree with a check of the kind, but in one place.
It is worth it when parts and wholes are nested in each other on however many levels come: boxes, folders, categories in a catalog, menus with submenus, elements on a page.
Exercise
The receipt
Solve the task in your editor.
The receipt as a tree. Add a method print(String indent) to the product and to the box, and to the interface too. The product prints its name and its price. The box prints its name and its total, and then calls print on every child with two more spaces. Print the order from the card "The total goes down to the bottom and comes back":
Order: €24.50 Candle: €5.00 Holiday box: €19.50 Tea: €4.50 Honey: €6.00 Small box: €7.00 Candy: €3.50 Chocolate: €2.50More tasks
- How many products. Add
count(): a product is 1, and a box is the total of its children. The packaging isn't a product. The order has 5. - Folders and files. Make a file with a name and a size in KB, and a folder with files and folders. Print the size of every folder. What does it have in common with the boxes?
- The most expensive product. Find the price of the most expensive product in the whole tree. Think about what an empty box returns.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Exceptions · Lists · record, switch and patterns · Design patterns
- Continues in: Command
- Tasks on this topic: Design patterns