Structural · DWG NO. 005

Decorator

Wrap an object to add behavior, layer by layer, without touching its class or its callers.

TypeStructural
ScopeSingle object, layered
ComplexityLow–Medium
Common inMiddleware, streams, UI widgets

Subclassing every combination doesn't scale

A `Coffee` class that needs milk, or syrup, or both, or double syrup, tempts you into `CoffeeWithMilk`, `CoffeeWithSyrup`, `CoffeeWithMilkAndSyrup`... The subclass count explodes combinatorially as more add-ons appear, and most of those classes only exist to combine two flags.

Wrap, don't multiply

A decorator implements the same interface as the object it wraps, holds a reference to the wrapped object, and calls through to it — adding its own behavior before or after. Decorators can wrap other decorators, so any combination is just a different nesting order, not a new class.

Coffee cost() MilkDecorator SyrupDecorator base Coffee wraps MilkDecorator( SyrupDecorator( Coffee ) )

Stackable coffee add-ons

Every decorator implements `cost()` and `describe()` and delegates to whatever it wraps. Combinations are just different nesting — no new class per combination.

coffee-decorator.js
class Coffee {
  cost() { return 2.5; }
  describe() { return "Coffee"; }
}

class MilkDecorator {
  constructor(wrapped) { this.wrapped = wrapped; }
  cost() { return this.wrapped.cost() + 0.5; }
  describe() { return `${this.wrapped.describe()} + milk`; }
}

class SyrupDecorator {
  constructor(wrapped) { this.wrapped = wrapped; }
  cost() { return this.wrapped.cost() + 0.75; }
  describe() { return `${this.wrapped.describe()} + syrup`; }
}

let order = new Coffee();
order = new SyrupDecorator(order);
order = new MilkDecorator(order);

console.log(order.describe()); // "Coffee + syrup + milk"
console.log(order.cost());     // 3.75

Trade-offs