design·lab

Structural pattern

Composite

Treat a single object and a whole group of objects through the exact same interface — so client code never needs to know which one it's holding.

✗ The problem

Every call site branches on the type

Client code special-cases leaves vs. containers everywhere it touches the tree.

function totalSize(node) {
  if (node.isFolder) {
    let sum = 0;
    for (const c of node.children)
      sum += totalSize(c);   // recurse… correctly?
    return sum;
  } else {
    return node.size;      // leaf case
  }
}
Every operation on the tree must branch on isFolder — and it's easy to get the recursion wrong, or forget a branch when a new node type shows up.
✓ The pattern

One interface, two implementations

A Leaf returns its own size. A Composite holds children and returns their sum — through the same size() call.

class File {
  constructor(kb) { this.kb = kb; }
  size() { return this.kb; }
}

class Folder {
  children = [];
  add(c) { this.children.push(c); }
  size() {
    return this.children.reduce(
      (t, c) => t + c.size(), 0);
  }
}
Component
size()
File
leaf
Folder
composite → children[]
✓ See it live

Click through the tree — then sum it in one call

Click a folder to expand/collapse it. Every node — file or folder — answers the same size() call; folders just add up their children's answers.

Total: — KB
one recursive call — no type checks
✓ Takeaway

One recursive call, no type checks

  • Uniform treatment: individual objects and compositions share one contract.
  • Natural for trees: filesystems, the DOM, menus, org charts — anywhere "one or many".
  • Client code stays flat: it calls size() once and never asks "which kind is this?"
  • Careful: deep trees can mean deep recursion; watch stack depth on huge structures.
🎯 Principle applied: Composite gives leaves and containers a shared contract — Liskov Substitution (they're interchangeable) and Open/Closed (add new node types without touching client code).