Skip to main content

Composite Pattern

Definition​

The Composite composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions uniformly β€” a single item and a group of items share the same interface.

Coffee Shop Example​

Our Coffee Shop has a menu with individual items (Latte, Croissant) and combo meals (Breakfast Combo, Family Bundle). Whether it's a single item or a combo, the system should calculate the price and display details the same way.

Structure​

Component Interface​

public interface IMenuComponent
{
string GetName();
decimal GetPrice();
string Print(string indent = "");
}

Leaf β€” Individual Menu Item​

public class MenuItem : IMenuComponent
{
public string Name { get; }
public decimal Price { get; }

public MenuItem(string name, decimal price)
{
Name = name;
Price = price;
}

public string GetName() => Name;
public decimal GetPrice() => Price;

public string Print(string indent = "") =>
$"{indent}- {Name}: ${Price:F2}";
}

Composite β€” Combo Meal​

public class MenuCombo : IMenuComponent
{
public string Name { get; }
private readonly List<IMenuComponent> _children = new();

public MenuCombo(string name)
{
Name = name;
}

public void Add(IMenuComponent component) => _children.Add(component);
public void Remove(IMenuComponent component) => _children.Remove(component);
public IReadOnlyList<IMenuComponent> GetChildren() => _children.AsReadOnly();

public string GetName() => Name;

public decimal GetPrice() => _children.Sum(c => c.GetPrice());

public string Print(string indent = "")
{
var sb = new StringBuilder();
sb.AppendLine($"{indent}+ {Name} (${GetPrice():F2})");
foreach (var child in _children)
sb.AppendLine(child.Print(indent + " "));
return sb.ToString().TrimEnd();
}
}

Client β€” Building the Menu​

// Individual items
var latte = new MenuItem("Latte", 4.50m);
var espresso = new MenuItem("Espresso", 2.50m);
var croissant = new MenuItem("Croissant", 3.00m);
var muffin = new MenuItem("Blueberry Muffin", 3.50m);
var juice = new MenuItem("Fresh Orange Juice", 4.00m);

// Breakfast Combo: Latte + Croissant
var breakfastCombo = new MenuCombo("Breakfast Combo");
breakfastCombo.Add(latte);
breakfastCombo.Add(croissant);

// Family Bundle: 2 Lattes + Espresso + 2 Croissants + Juice
var familyBundle = new MenuCombo("Family Bundle");
familyBundle.Add(latte);
familyBundle.Add(latte); // second latte
familyBundle.Add(espresso);
familyBundle.Add(croissant);
familyBundle.Add(croissant); // second croissant
familyBundle.Add(juice);

// Nested combo β€” a "Super Bundle" containing combos
var superBundle = new MenuCombo("Super Bundle");
superBundle.Add(breakfastCombo);
superBundle.Add(familyBundle);
superBundle.Add(muffin); // extra item

// Print everything β€” single items and combos are treated the same
var allItems = new IMenuComponent[] { latte, breakfastCombo, familyBundle, superBundle };
foreach (var item in allItems)
{
Console.WriteLine(item.Print());
Console.WriteLine($" Total: ${item.GetPrice():F2}");
Console.WriteLine();
}

// Output:
// - Latte: $4.50
// Total: $4.50
//
// + Breakfast Combo ($7.50)
// - Latte: $4.50
// - Croissant: $3.00
// Total: $7.50
//
// + Family Bundle ($21.50)
// - Latte: $4.50
// - Latte: $4.50
// - Espresso: $2.50
// - Croissant: $3.00
// - Croissant: $3.00
// - Fresh Orange Juice: $4.00
// Total: $21.50
//
// + Super Bundle ($32.00)
// + Breakfast Combo ($7.50)
// - Latte: $4.50
// - Croissant: $3.00
// + Family Bundle ($21.50)
// - Latte: $4.50
// - Latte: $4.50
// - Espresso: $2.50
// - Croissant: $3.00
// - Croissant: $3.00
// - Fresh Orange Juice: $4.00
// - Blueberry Muffin: $3.50
// Total: $32.00
tip

Notice the tree structure: SuperBundle β†’ [BreakfastCombo, FamilyBundle, Muffin]. Combos can contain other combos β€” the recursive GetPrice() and Print() handle nesting naturally.

Transparent vs Safe Composite​

Transparent (default)Safe
Child managementAdd/Remove in interfaceOnly in MenuComposite
Leaf behaviorLeaf throws NotSupportedExceptionLeaf doesn't have Add/Remove
Client simplicityClient doesn't need to check typesClient must check type before Add/Remove
Trade-offSimpler client code, runtime error riskSafer, but client needs type checking

.NET Real-World Usage​

  • System.Windows.Forms.Control β€” controls contain child controls (Controls collection)
  • WPF Panel β€” panels contain child UI elements in a tree
  • File system APIs β€” directories contain files and subdirectories
  • IConfigurationSection β€” configuration sections contain child sections

When to Use​

  • You need to represent a part-whole hierarchy (menu β†’ items, directory β†’ files)
  • You want clients to ignore the difference between individual items and groups
  • The structure is naturally recursive or tree-like

When NOT to Use​

  • The hierarchy is flat (no nesting) β€” a simple list suffices
  • Individual items and groups have very different behavior β€” forcing them into one interface creates awkward design
  • You need type safety for add/remove operations at compile time

Key Takeaways​

  • Composite treats individuals and groups uniformly through a shared interface
  • Tree structures are handled naturally with recursive operations
  • Adding new item types or new combo types doesn't change client code (OCP)
  • Leaf and composite share the same interface β€” client code stays simple

Interview Questions​

Q: How do you handle operations that only make sense for composites (like Add/Remove)? Two approaches: Transparent β€” put Add/Remove in the interface, have leaf throw NotSupportedException. Safe β€” only put Add/Remove on the composite class, forcing clients to check types. Transparent is more common in practice.

Q: Is Composite just a tree data structure? The tree structure is part of it, but the key insight is the uniform interface. Without it, you'd need if (item is MenuCombo) checks everywhere. Composite eliminates that branching by making the interface identical.

Q: How does Composite relate to the DOM or file systems? Both are classic Composite examples. HTML DOM elements contain child elements. Directories contain files and subdirectories. In both cases, you can treat a single node and a subtree uniformly.

  • Decorator β€” similar structure (wraps a component) but adds behavior, doesn't aggregate
  • Iterator β€” traverses composite structures without exposing internals
  • Visitor β€” adds operations to a composite without modifying the classes
  • SOLID β€” OCP β€” new component types don't break existing code