Design Patterns

Director Design Pattern

I was adding features to a coffee ordering system. A basic coffee costs $5. Add milk, that's $2 more. Add vanilla syrup, another $1.50. Whipped cream? $1....

23 Mar 2024

Director Design Pattern

I was adding features to a coffee ordering system. A basic coffee costs $5. Add milk, that's $2 more. Add vanilla syrup, another $1.50. Whipped cream? $1. If I subclassed every combination — CoffeeWithMilk, CoffeeWithMilkAndVanilla, CoffeeWithMilkAndVanillaAndWhip — the class count would explode.

The Decorator pattern lets you stack behaviors onto an object at runtime, without subclassing. Each decorator wraps the original object, adds its behavior, and delegates the rest. You can combine them in any order.

Think of it like putting stickers on a laptop. Each sticker adds something. You can add as many as you want, in any order. The laptop underneath doesn't change.

Javascript
class Coffee {
  getCost() {
    return 5;
  }

  getDescription() {
    return "Coffee";
  }
}

class CoffeeDecorator extends Coffee {
  constructor(coffee) {
    super();
    this.coffee = coffee;
  }

  getCost() {
    return this.coffee.getCost();
  }

  getDescription() {
    return this.coffee.getDescription();
  }
}

class MilkDecorator extends CoffeeDecorator {
  getCost() {
    return this.coffee.getCost() + 2;
  }

  getDescription() {
    return this.coffee.getDescription() + ", Milk";
  }
}

class VanillaDecorator extends CoffeeDecorator {
  getCost() {
    return this.coffee.getCost() + 1.5;
  }

  getDescription() {
    return this.coffee.getDescription() + ", Vanilla";
  }
}

// Usage
let order = new Coffee();
order = new MilkDecorator(order);
order = new VanillaDecorator(order);

console.log(order.getDescription()); // Coffee, Milk, Vanilla
console.log(order.getCost());        // 8.5

Each decorator wraps the previous object. MilkDecorator wraps Coffee. VanillaDecorator wraps the milk-wrapped coffee. When you call getCost(), it chains through all the layers.

The key: the client doesn't know how many decorators are stacked. It just calls getCost() on whatever it has.

The benefit: You compose behavior dynamically. No class explosion. Adding a new option means one new decorator class. The original object stays untouched — this follows the Open/Closed Principle.

The cost: Lots of small wrapper objects. Deep decorator chains are hard to debug — stack traces get long and the wrapping order can matter. If you need to inspect or remove a specific decorator in the middle, it's awkward.

I use decorators for logging wrappers, caching layers, permission checks, and stream transformations. If you find yourself creating subclasses for every combination of features, decorators are the escape hatch.

Keep reading