The Composite is a pattern that treats individual objects and compositions through a common interface. It is especially useful for modeling tree structures.
In software development, we often need to model hierarchies: menus with submenus, file systems, HTML structure (DOM), or vector graphics where you group shapes.
The problem arises when we want to perform an operation on the entire structure, but the “nodes” (leaves) and “containers” (branches) are different classes.
The Problem: The Complex Order
Imagine we are working on the logistics of an online store. We need to calculate the total price of a shipment.
A shipment can be simple:
- A
Product(e.g., Headphones - 50€).
Or complex:
- A
Boxthat contains: - A
Product(Hammer). - A
Product(Nails). - Another
Box(small tool pack) that contains: - Screwdriver.
- Tape measure.
If we don’t use patterns, our price calculation code would be a nightmare of type checking:
// ❌ Painful code without Composite
public double CalculatePrice(object item)
{
if (item is Product p)
{
return p.Price;
}
else if (item is Box b)
{
double total = 0;
foreach (var subItem in b.Contents)
{
// Manual recursion and constant type checking!
total += CalculatePrice(subItem);
}
return total;
}
}This violates the Open/Closed Principle. If tomorrow we add a “Pallet” or “Container”, we have to rewrite the calculation logic.
The Solution: “Treat them all the same”
The Composite pattern suggests that we create a common base class (or interface) for both individual elements (Leaf) and containers (Composite).
This way, the container doesn’t care if what’s inside is a simple product or a giant box. It just calls the GetPrice() method and lets polymorphism do its work.
Implementation in C#
Let’s solve the logistics shipment problem.
We define the abstract class that represents anything that can be shipped.
public abstract class ShipmentItem
{
protected string _name;
public ShipmentItem(string name)
{
_name = name;
}
// The common operation
public abstract double GetPrice();
// Optional operations for managing children
// (We can leave them empty or throw an error by default)
public virtual void Add(ShipmentItem item)
{
throw new NotImplementedException();
}
public virtual void Remove(ShipmentItem item)
{
throw new NotImplementedException();
}
}The simple product. It has no children, just a price.
public class Product : ShipmentItem
{
private double _price;
public Product(string name, double price) : base(name)
{
_price = price;
}
public override double GetPrice()
{
Console.WriteLine($" - Product: {_name} ({_price}€)");
return _price;
}
}The box. Notice the detail in the GetPrice method: it uses implicit recursion.
public class Box : ShipmentItem
{
// The key to the pattern: A list of BASE TYPES
private List<ShipmentItem> _contents = new List<ShipmentItem>();
public Box(string name) : base(name) { }
public override void Add(ShipmentItem item)
{
_contents.Add(item);
}
public override void Remove(ShipmentItem item)
{
_contents.Remove(item);
}
public override double GetPrice()
{
Console.WriteLine($"Opening box: {_name}...");
double totalPrice = 0;
// We iterate over the children. We DON'T know if they are boxes or products.
// And we don't care. We only know they have GetPrice().
foreach (var item in _contents)
{
totalPrice += item.GetPrice();
}
// Suppose the box itself costs 1€ for the cardboard
return totalPrice + 1.0;
}
}We can create structures as deep as we want.
class Program
{
static void Main(string[] args)
{
// 1. Create individual products
var headphones = new Product("Sony Headphones", 50);
var charger = new Product("USB Charger", 15);
var hammer = new Product("Thor Hammer", 25);
// 2. Create a small box (Composite)
var techBox = new Box("Tech Pack");
techBox.Add(headphones);
techBox.Add(charger);
// 3. Create a BIG box that contains products AND ANOTHER BOX
var finalOrder = new Box("Amazon Order");
finalOrder.Add(hammer); // Add a Leaf
finalOrder.Add(techBox); // Add a Composite
// 4. Calculate the total
Console.WriteLine("--- Calculating Order Total ---");
double total = finalOrder.GetPrice();
Console.WriteLine($"\nTotal Price to pay: {total}€");
}
}Output:
--- Calculating Order Total ---
Opening box: Amazon Order...
- Product: Thor Hammer (25€)
Opening box: Tech Pack...
- Product: Sony Headphones (50€)
- Product: USB Charger (15€)
Total Price to pay: 92€(90€ of products + 1€ small box + 1€ big box)
Why is it so powerful?
The best thing about this pattern is that it greatly simplifies the client. The client doesn’t need to worry about traversing the tree. It calls the method on the root, and the call cascades through all branches down to the last leaves.
Typical Use Cases
- Graphic Systems: A “Group” of shapes in PowerPoint. You can move the group, or scale the group, and all the lines and rectangles inside are moved.
- File Systems:
GetSize()orDelete()work the same for folders and files. - User Interfaces (WPF/WinForms): A
PanelcontainsButtonsand otherPanels. When you render the main panel, all its children are rendered.
The Composite pattern allows us to create complex structures from simple objects, maintaining a uniform interface. It is the definition of recursive elegance.