Low-level design2 min

Composite Pattern

Type: Structural Pattern
Relevance: Medium. Specifically useful for hierarchical tree structures.

The Composite pattern lets you compose objects into tree structures to represent part-whole hierarchies. It allows clients to treat individual objects and compositions of objects uniformly.

Real-Life Analogy

Think of an Army. An army consists of Generals, who command Colonels, who command Captains, who command individual Soldiers. If you want to give a command to "Move Forward", you don't want to loop through 10,000 individual soldiers. You give the command to the General, who passes it down the tree. From the client's perspective, issuing an order to a single Soldier or to an entire Battalion looks exactly the same.

Another analogy: A File System. A Folder contains Files and other Folders. If you call getSize() on a Folder, it recursively calculates the size of everything inside it.

The Problem

If your application's core model can be represented as a tree, and you have to write code that checks if (object instanceof Folder) { loop through children } else { calculate size }, the code becomes messy and strongly coupled to the concrete classes.

How to Implement Composite

Define a common interface for both the simple elements (Leaves) and the complex elements (Composites).

Example in Code

typescript
// 1. The Component Interface
interface Graphic {
  draw(): void;
}

// 2. The Leaf (Simple Element)
class Dot implements Graphic {
  constructor(private x: number, private y: number) {}
  
  draw(): void {
    console.log(`Drawing Dot at (${this.x}, ${this.y})`);
  }
}

// 3. The Composite (Complex Element)
class GraphicGroup implements Graphic {
  private children: Graphic[] = [];

  public add(child: Graphic): void {
    this.children.push(child);
  }

  public remove(child: Graphic): void {
    this.children = this.children.filter(c => c !== child);
  }

  // The Composite delegates the work to its children
  public draw(): void {
    console.log("Drawing Group:");
    for (const child of this.children) {
      child.draw(); 
    }
  }
}

// Client Code
const dot1 = new Dot(1, 2);
const dot2 = new Dot(3, 4);

const group1 = new GraphicGroup();
group1.add(dot1);
group1.add(dot2);

const dot3 = new Dot(5, 6);
const mainGroup = new GraphicGroup();
mainGroup.add(group1); // Adding a group to a group!
mainGroup.add(dot3);

// The client treats the massive group EXACTLY like a single dot!
mainGroup.draw(); 

Class Diagram

Why is it Important?

  • Uniformity: Clients don't care if they are dealing with a single object or a massive tree of objects. They use the exact same interface.
  • Extensibility: You can easily add new element types to the tree without breaking existing code.