Decorator

Intermediate · 11 min read · ▶ live playground · ✦ checkpoint

The coffee menu starts with CoffeeWithMilk and CoffeeWithSugar. Foam arrives, and the class list turns into CoffeeWithMilkAndSugarAndFoam, plus another subclass for every order the add-ons can appear in.

The Decorator pattern attaches responsibilities to an object dynamically by wrapping it in another object with the same interface. It replaces combination subclasses with runtime composition, but a deep wrapper chain creates many small objects and can make control flow harder to inspect.

Before: subclasses encode every add-on combination

Inheritance can describe one fixed variation. It becomes awkward when each add-on is optional and several can be combined:

class Coffee {
  public cost(): number {
    return 2.5;
  }
 
  public description(): string {
    return "Coffee";
  }
}
 
class CoffeeWithMilk extends Coffee {
  public cost(): number {
    return 3.1;
  }
 
  public description(): string {
    return "Coffee, milk";
  }
}
 
class CoffeeWithSugar extends Coffee {
  public cost(): number {
    return 2.9;
  }
 
  public description(): string {
    return "Coffee, sugar";
  }
}
 
class CoffeeWithMilkAndSugar extends Coffee {
  public cost(): number {
    return 3.5;
  }
 
  public description(): string {
    return "Coffee, milk, sugar";
  }
}

The combined subclass repeats the prices and labels already encoded by the single-add-on subclasses. Double milk needs another class or a parameter that only some subclasses understand. Reversing the add-on order may require yet another name if order affects behavior.

With three optional add-ons, there can be eight subsets before order or quantity enters the picture. This is another combinatorial blowup caused by using inheritance to represent choices that should be composed.

After: keep the same interface through every layer

A decorator is a wrapper that implements the same interface as the object it contains and augments a delegated result. The wrapped component is the one Beverage object held by that wrapper. A concrete decorator such as Milk or Sugar defines one responsibility to add.

Espresso is the concrete base beverage. BeverageDecorator conforms to Beverage through the abstract BeverageComponent base, then holds another Beverage. That self-reference lets a decorator wrap the base or another decorator.

class diagram

Beverage decorators preserve the interface while adding layers

Beverage decorators preserve the interface while adding layers
  1. 1Beverage is the interface clients use before and after decoration.
  2. 2BeverageComponent makes that shared conformance explicit for both branches.
  3. 3Espresso is a concrete component that supplies the starting cost and description.
  4. 4BeverageDecorator remains substitutable for any BeverageComponent.
  5. 5The decorator also owns one Beverage, which may already be another decorated object.
  6. 6Milk delegates to the wrapped beverage, then adds its own description and price.
  7. 7Sugar is another independent layer; callers choose the stack at runtime.

BeverageComponent is a thin abstract base, not a required extra role. Espresso and BeverageDecorator could implement Beverage directly. The essential relationship is that the decorator exposes Beverage while its wrapped field also has type Beverage.

interface Beverage {
  cost(): number;
  description(): string;
}
 
abstract class BeverageComponent implements Beverage {
  public abstract cost(): number;
  public abstract description(): string;
}
 
class Espresso extends BeverageComponent {
  public cost(): number {
    return 2.5;
  }
 
  public description(): string {
    return "Espresso";
  }
}
 
abstract class BeverageDecorator extends BeverageComponent {
  protected wrapped: Beverage;
 
  public constructor(wrapped: Beverage) {
    super();
    this.wrapped = wrapped;
  }
}
 
class Sugar extends BeverageDecorator {
  public cost(): number {
    return this.wrapped.cost() + 0.4;
  }
 
  public description(): string {
    return this.wrapped.description() + ", sugar";
  }
}
 
class Milk extends BeverageDecorator {
  public cost(): number {
    return this.wrapped.cost() + 0.6;
  }
 
  public description(): string {
    return this.wrapped.description() + ", milk";
  }
}
 
const order: Beverage = new Milk(
  new Sugar(
    new Espresso(),
  ),
);
 
console.log(order.description());
console.log(`$${order.cost().toFixed(2)}`);

Espresso, sugar, milk $3.50

The outer Milk receives the call first. It delegates to Sugar, which delegates to Espresso. Results then return outward: Sugar adds $0.40 and its label, and Milk adds $0.60 and its label.

The final object still has type Beverage, so checkout does not need a branch for Milk, Sugar, or their order. Double milk is new Milk(new Milk(new Espresso())); no DoubleMilkEspresso subclass is required.

Inheritance still appears between Milk and BeverageDecorator, but it is not the source of combination flexibility. Composition builds the stack at runtime, in any order and quantity. Each concrete decorator owns one small addition instead of a whole menu combination.

Decorator beside the other structural patterns

These patterns can share nearly identical arrows, so follow the intent:

  • Decorator wraps one object through the same interface and adds a responsibility.
  • Adapter wraps an incompatible object and presents a different target interface.
  • Composite owns many objects through the same interface and combines their results as a tree.
  • Bridge holds an implementation from a separate variation axis; it does not accumulate layers of the abstraction's interface.

One class can participate in more than one pattern, but the design pressure still gives the clearest name. A logging wrapper around PaymentGateway is a decorator; a wrapper that turns chargeCard(cents) into PaymentGateway.charge(dollars) is an adapter.

When NOT to use it

Deep decoration is difficult to debug. A call may cross six tiny objects before reaching the base, stack traces expose infrastructure rather than the business idea, and object identity no longer tells you which responsibilities are present. Order can also change the result: compress-then-encrypt is not the same pipeline as encrypt-then-compress.

If the behavior set is fixed and small, parameters may be clearer. new Coffee({ milk: true, sugar: false }) puts a stable menu in one visible value. A small subclass can also communicate one permanent variation better than a runtime wrapper graph.

Decorator carries allocation and testing costs as well. Each layer needs focused tests, and meaningful order combinations need integration tests. Use it when responsibilities are independently optional, must stack, or need runtime selection—not whenever one method adds another line to a result.

Checkpoint

Answer all three to mark this lesson complete

Decorator lets one object gather responsibilities layer by layer. Next, Facade moves in the opposite direction by offering one small entry point over a subsystem of many objects.

+50 XP on completion