Decorator Pattern
Concept
Category: Structural Pattern
The Decorator Pattern allows behavior to be added to an individual object, dynamically at runtime, without affecting the behavior of other objects from the same class.
It is designed to solve the “Subclass Explosion” problem.
The Mental Model (The Coffee Shop)
Imagine you own a Coffee Shop. You create a base class Coffee (cost: 3).
Some want Sugar. You create CoffeeWithSugar extends Coffee.
Some want both! You create CoffeeWithMilkAndSugar extends Coffee.
What if you introduce Vanilla, Caramel, and Whipped Cream?
To cover every possible combination of ingredients using Inheritance (subclassing), you would have to physically write thousands of classes: CoffeeWithMilkAndVanillaAndWhip, CoffeeWithSugarAndCaramel. This is the Subclass Explosion.
The Decorator Solution:
Instead of subclassing, you put the Coffee object inside a Milk object! Then you put the Milk object inside a Vanilla object! Like Russian Nesting Dolls.
When you ask the outermost doll for the price, it asks the doll inside it, which asks the doll inside it, all the way down to the base Coffee, adding up the prices as it bubbles back out!
Implementation
The key to the Decorator Pattern is that the Decorator MUST implement the exact same Interface as the object it is wrapping. This ensures that the outside world doesn’t even know the object has been wrapped!
// 1. The Base Interface
interface Beverage {
getDescription(): string;
getCost(): number;
}
// 2. The Core Component
class SimpleCoffee implements Beverage {
getDescription() { return "Simple Coffee"; }
getCost() { return 2.00; }
}
// 3. The Base Decorator Class
// It implements Beverage, BUT it also holds a reference to a Beverage inside it!
abstract class BeverageDecorator implements Beverage {
protected wrappee: Beverage;
constructor(beverage: Beverage) {
this.wrappee = beverage; // Swallow the doll!
}
// Default delegation
getDescription() { return this.wrappee.getDescription(); }
getCost() { return this.wrappee.getCost(); }
}
// 4. Concrete Decorators (The Ingredients)
class MilkDecorator extends BeverageDecorator {
getDescription() {
// Get the internal description, and APPEND to it!
return this.wrappee.getDescription() + ", Milk";
}
getCost() {
// Get the internal cost, and ADD to it!
return this.wrappee.getCost() + 0.50;
}
}
class CaramelDecorator extends BeverageDecorator {
getDescription() { return this.wrappee.getDescription() + ", Caramel"; }
getCost() { return this.wrappee.getCost() + 0.75; }
}
// Usage:
// 1. Start with a base coffee
let myDrink: Beverage = new SimpleCoffee();
console.log(`${myDrink.getDescription()} | $${myDrink.getCost()}`);
// "Simple Coffee | $2.00"
// 2. Wrap it in Milk!
myDrink = new MilkDecorator(myDrink);
console.log(`${myDrink.getDescription()} | $${myDrink.getCost()}`);
// "Simple Coffee, Milk | $2.50"
// 3. Wrap it in Caramel!
myDrink = new CaramelDecorator(myDrink);
console.log(`${myDrink.getDescription()} | $${myDrink.getCost()}`);
// "Simple Coffee, Milk, Caramel | $3.25"
Real-World Usage
The Decorator pattern is massively prevalent in backend APIs.
If you have a UserRepository.getUser() function, you might wrap it in a LoggingDecorator (which logs the request to the terminal, then calls the real getUser), and then wrap that in a CachingDecorator (which checks Redis first, and if empty, calls the internal LoggingDecorator).
Interview Questions
Q: TypeScript and Python have @Decorator syntax. Is this the same thing?
A: Yes! The @ syntax is literally just syntactic sugar for the exact same architectural pattern.
When you write @memoize above a Python function, the Python interpreter takes your function, physically wraps it inside the memoize caching function, and replaces your function with the wrapped version at runtime. It is the Russian Nesting Doll implemented at the compiler level.
Q: What is the difference between the Decorator Pattern and the Adapter Pattern?
A: Structurally, they both wrap an object. The difference is intent.
- A Decorator wraps an object to add new behaviors/features, but strictly maintains the exact same interface (A
Beveragegoes in, aBeveragecomes out). - An Adapter wraps an object to change its interface entirely, allowing incompatible systems to talk to each other. (An XML stream goes in, a JSON object comes out).