Skip to content

Composite Design Pattern (Structural)

Composite is a structural design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects.

Using the Composite pattern makes sense only when the core model of your app can be represented as a tree.

For example, imagine you have two types of objects: Products and Boxes. A Box can contain several Products as well as a number of smaller Boxes. These little Boxes can also hold some Products or even smaller Boxes, and so on.

Say you decide to create an ordering system that uses these classes. Orders could contain simple products without any packaging, as well as boxes stuffed with products…and other boxes. How would you determine the total price of such an order?

The Composite pattern suggests that you work with Products and Boxes through a common interface which declares a method for calculating the total price.

For a product, it’d simply return the product’s price. For a box, it’d go over each item the box contains, ask its price and then return a total for this box. If one of these items were a smaller box, that box would also begin going over its contents and so on, until the prices of all inner components were calculated.

An Army is a great example of a composite structure. An army consists of several divisions; a division is a set of brigades, and a brigade consists of platoons, which can be broken down into squads. Finally, a squad is a small group of real soldiers. Orders are given at the top of the hierarchy and passed down through each level until every soldier knows what has to be done.

  • Component: The interface describes operations that are common to both simple and complex elements of the tree.
  • Leaf: A basic element of a tree that doesn’t have sub-elements.
  • Composite (Container): An element that has sub-elements: leaves or other composites. A container doesn’t know the concrete classes of its children.
  • Client: Works with all elements through the component interface.
classDiagram
    class Component {
        <<interface>>
        +execute()
    }
    class Leaf {
        +execute()
    }
    class Composite {
        -children: List<Component>
        +add(c: Component)
        +remove(c: Component)
        +execute()
    }

    Component <|-- Leaf
    Component <|-- Composite
    Composite o-- Component : children
# The component interface declares common operations for both
# simple and complex objects of a composition.
interface Graphic:
method move(x, y)
method draw()
# The leaf class represents end objects of a composition.
class Dot implements Graphic:
field x, y
constructor Dot(x, y) { ... }
method move(x, y) { this.x += x, this.y += y }
method draw() { # Draw a dot at X and Y. }
# The composite class represents complex components that may
# have children.
class CompoundGraphic implements Graphic:
field children: array of Graphic
method add(child: Graphic) { add child to children }
method remove(child: Graphic) { remove child from children }
method move(x, y):
foreach (child in children):
child.move(x, y)
method draw():
# 1. For each child element:
# 2. Draw the element.
  • Tree-like structures: Use the pattern when you have to implement a tree-like object structure.
  • Uniformity: Use the pattern when you want the client code to treat both simple and complex elements uniformly.
  1. Make sure that the core model of your app can be represented as a tree structure.
  2. Declare the component interface with a list of methods that make sense for both simple and complex components.
  3. Create a leaf class to represent simple elements.
  4. Create a container class to represent complex elements, including an array field for storing references to sub-elements.
  5. Finally, define the methods for adding and removal of child elements in the container.

Pros:

  • Polymorphism: You can work with complex tree structures more conveniently using recursion.
  • Open/Closed Principle: You can introduce new element types into the app without breaking existing code.

Cons:

  • Over-generalization: It might be difficult to provide a common interface for classes whose functionality differs too much.
  • Chain of Responsibility is often used in conjunction with Composite. In this case, when a leaf component gets a request, it may pass it through the chain of all of the parent components down to the root of the object tree.
  • You can use Iterators to traverse Composite trees.
  • You can use Visitor to execute an operation over an entire Composite tree.

// Component
public abstract class FileSystemItem
{
public string Name { get; }
protected FileSystemItem(string name)
{
Name = name;
}
public abstract void Display(int depth);
}
// Leaf
public class File : FileSystemItem
{
public File(string name) : base(name) {}
public override void Display(int depth)
{
Console.WriteLine(new string('-', depth) + Name);
}
}
// Composite
public class Directory : FileSystemItem
{
private readonly List&lt;FileSystemItem&gt; _items = new();
public Directory(string name) : base(name) {}
public void Add(FileSystemItem item) => _items.Add(item);
public override void Display(int depth)
{
Console.WriteLine(new string('-', depth) + Name);
foreach (var item in _items)
item.Display(depth + 2);
}
}
// Usage
class Program
{
static void Main()
{
var root = new Directory("root");
root.Add(new File("file1.txt"));
root.Add(new File("file2.txt"));
var folder = new Directory("docs");
folder.Add(new File("resume.pdf"));
folder.Add(new File("notes.txt"));
root.Add(folder);
root.Display(1);
}
}