Wrap an object to add behavior, layer by layer, without touching its class or its callers.
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.
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.
Every decorator implements `cost()` and `describe()` and delegates to whatever it wraps. Combinations are just different nesting — no new class per combination.
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