Skip to content
wannacode

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, decimal Price);
record Box(string Name, decimal 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 decimal Total(List<object> order)
{
decimal sum = 0;
foreach (object item in order)
{
if (item is Product p)
{
sum += p.Price;
}
else if (item is Box box)
{
sum += box.Packaging;
foreach (object inner in box.Items)
{
if (inner is Product q)
{
sum += q.Price;
}
}
}
}
return sum;
}
List<object> order = [
new Product("Candle", 5.00m),
new Box("Holiday box", 2.00m, [
new Product("Tea", 4.50m),
new Product("Honey", 6.00m),
new Box("Small box", 1.00m, [
new Product("Candy", 3.50m),
new Product("Chocolate", 2.50m)])])];
Console.WriteLine($"Total: €{Total(order):F2}");
Output
Total: €17.50

The 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 interface Item (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 IItem
{
decimal Total();
}

The product and the box. The constructor of the box takes any number of items (params IItem[] items):

record Product(string Name, decimal Price) : IItem
{
public decimal Total() => Price;
}
class Box : IItem
{
private readonly List<IItem> items;
public string Name { get; }
public decimal Packaging { get; }
public Box(string name, decimal packaging, params IItem[] items)
{
Name = name;
Packaging = packaging;
this.items = new List<IItem>(items);
}
public decimal Total()
{
decimal sum = Packaging;
foreach (IItem item in items)
{
sum += item.Total();
}
return sum;
}
}

The tree is written as nested constructors, one item per line:

IItem order = new Box("Order", 0,
new Product("Candle", 5.00m),
new Box("Holiday box", 2.00m,
new Product("Tea", 4.50m),
new Product("Honey", 6.00m),
new Box("Small box", 1.00m,
new Product("Candy", 3.50m),
new Product("Chocolate", 2.50m))));
Console.WriteLine($"Total: €{order.Total():F2}");
Output
Total: €24.50

IItem order = new Box("Order", 0,
new Product("Candle", 5.00m),
new Box("Holiday box", 2.00m,
new Product("Tea", 4.50m),
new Product("Honey", 6.00m),
new Box("Small box", 1.00m,
new Product("Candy", 3.50m),
new Product("Chocolate", 2.50m))));
Console.WriteLine($"Total: €{order.Total():F2}");
Step by step, in words
  1. The tree is put together: every item is on its own line, and every box holds its children.
  2. Total() asks the root. The order doesn't know what is inside: it starts from its packaging and calls Total() on every child.
  3. Candle is a product: it answers with its price, 5.00, and the answer goes back up.
  4. Holiday box is a box: it does exactly the same, packaging first, then one child at a time.
  5. Small box is on the third level. Nothing disappears here: it asks its own children.
  6. Small box: 1.00 + 3.50 + 2.50 = 7.00. Its total goes up to its parent.
  7. Holiday box: 2.00 + 4.50 + 6.00 + 7.00 = 19.50.
  8. 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, decimal PerKg, decimal Kg) : IItem
{
public decimal Total() => PerKg * Kg;
}
IItem coffee = new Box("Coffee box", 1.00m,
new WeighedProduct("Coffee beans", 24.00m, 0.25m),
new Product("Mug", 8.00m));
Console.WriteLine($"Coffee box: €{coffee.Total():F2}");
Output
Coffee box: €15.00

Not 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 IItem, and text in place of a product doesn't compile:

IItem box = new Box("Box", 1.00m, "Tea");
Error
error CS1503: Argument 3: cannot convert from 'string' to 'IItem'
More 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?

IItem box = new Box("Box", 1.00m,
new Box("Empty", 0.50m),
new Product("Candle", 5.00m));
Console.WriteLine($"Box: €{box.Total():F2}");
Answer

Try it

Find the bug

Someone rewrites the box to make it "safer":

class Box : IItem
{
private readonly List<IItem> items;
public string Name { get; }
public decimal Packaging { get; }
public Box(string name, decimal packaging, params IItem[] items)
{
Name = name;
Packaging = packaging;
this.items = new List<IItem>(items);
}
public decimal Total()
{
decimal sum = Packaging;
foreach (IItem item in items)
{
if (item is Product)
{
sum += item.Total();
}
}
return sum;
}
}
IItem gift = new Box("Holiday box", 2.00m,
new Product("Tea", 4.50m),
new Product("Honey", 6.00m),
new Box("Small box", 1.00m,
new Product("Candy", 3.50m),
new Product("Chocolate", 2.50m)));
Console.WriteLine($"Holiday box: €{gift.Total():F2}");
Output
Holiday box: €12.50

The holiday box costs €19.50, and the program says €12.50. Which line is the bug on?

Pick the line with the bug.

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":

Output
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.50

More 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

1. The shop starts selling coffee by the kilo. What do you write with Composite?
2. How many levels can the tree have for the total to be right?
3. A box adds its packaging and the prices of only the products directly inside it. What happens to a box in the box?
4. When don't you need Composite?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·