The Decorator Pattern - Wrapping Behaviour Without Inheritance
The Decorator Pattern
The Decorator pattern attaches extra behaviour to an object without changing its class. You wrap an object in one or more layers, each adding a small capability, and the layers stack like onions. This beats subclassing when answer combinations explode.
The Problem: Subclass Explosion
A coffee shop serves plain coffee. Customers add milk, sugar, or whipped cream. Modelling every combination as a separate class gives you Latte, SugaredLatte, CreamLatte, SugaredCreamLatte, and so on. With six add-ons you approach 64 classes. Layers invert this: one base class plus one wrapper per add-on.
coffee (base, costs 2.0)
|
> + Milk (adds 0.5)
|
> + Sugar (adds 0.2)
|
> + WhippedCream (adds 0.7)
Final price = 2.0 + 0.5 + 0.2 + 0.7
Every decoration wraps what came before, and cost() adds its own amount then delegates.
The Core Classes
Start with an abstraction, a concrete base, and a decorator base.
public interface Coffee {
double cost();
String name();
}
public class PlainCoffee implements Coffee {
public double cost() { return 2.0; }
public String name() { return "Coffee"; }
}
public abstract class AddOnDecorator implements Coffee {
protected Coffee coffee;
public AddOnDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
The decorator base implements the interface and holds the wrapped coffee. It exists so subclasses share a constructor in one place.
Concrete Decorators
Each real add-on extends the decorator base.
public class Milk extends AddOnDecorator {
public Milk(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.5; }
public String name() { return coffee.name() + " with Milk"; }
}
public class Sugar extends AddOnDecorator {
public Sugar(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.2; }
public String name() { return coffee.name() + " with Sugar"; }
}
Walkthrough: cost() first asks the wrapped coffee how much it costs, then adds its own price. name() appends its ingredient to the wrapped name. Because every layer returns a Coffee, any layer can be decorated again, and the whole stack stays usable through the same interface.
Composing the Stack
The caller picks the toppings at runtime and stacks them freely.
Coffee drink = new PlainCoffee();
drink = new Milk(drink);
drink = new Sugar(drink);
System.out.println(drink.name() + " costs " + drink.cost());
The result is Coffee with Milk with Sugar. Adding a new topping later means one new class, not a family of combinations.
A Real-World Case
Java's own I/O library is built on decorators - BufferedReader wraps any Reader to add buffering, and LineNumberReader wraps that to add line counting. HTTP middleware stacks the same way: requests pass through auth, logging, and rate-limit layers in order, each wrapping the next. The pattern shines wherever features are optional, a mix-and-match is expected, and order matters.
Key Points
- Decorator adds behaviour by wrapping objects in layers.
- Each layer implements the same interface as the wrapped object.
- It beats inheritance when combinations would explode into dozens of classes.
- Layers can be added at runtime in any order.
- Cost() and name() show the pattern: add your part, then delegate.
- Java I/O and HTTP middleware are decorated in real frameworks today.