Decorator
In short: Decorator wraps an object in another object with the same interface and adds behaviour without changing the class and without a class for every combination.
Problem
The menu that grows with every add-on
The coffee shop sells espresso for €2. A customer can add milk (+€0.40), syrup (+€0.60) and an extra shot (+€0.80). Someone writes a class for every combination:
class Espresso { public double cost() { return 2.00; }
public String description() { return "Espresso"; }}
class EspressoWithMilk extends Espresso { @Override public double cost() { return super.cost() + 0.40; }
@Override public String description() { return super.description() + ", milk"; }}
class EspressoWithMilkAndSyrup extends EspressoWithMilk { @Override public double cost() { return super.cost() + 0.60; }
@Override public String description() { return super.description() + ", syrup"; }}Espresso drink = new Espresso();System.out.printf("%s: €%.2f%n", drink.description(), drink.cost());drink = new EspressoWithMilk();System.out.printf("%s: €%.2f%n", drink.description(), drink.cost());drink = new EspressoWithMilkAndSyrup();System.out.printf("%s: €%.2f%n", drink.description(), drink.cost());Espresso: €2.00Espresso, milk: €2.40Espresso, milk, syrup: €3.00It works for the three drinks on the menu. But three add-ons make eight combinations, and with cappuccino and tea that's 24 classes. One more add-on doubles the count.
EspressoWithSyrup (without milk) can't inherit the syrup from EspressoWithMilkAndSyrup, because a class has only one parent. The syrup code gets copied, and when syrup gets more expensive, it changes in several places.And the customer wants syrup twice. There is no such class. A class for one, two and three syrups is not a solution.
The idea
Decorator: a wrapper with the same shape
An add-on isn't a kind of drink. It is something that wraps a drink: it takes the drink and adds its own part.
So they all share one shape: the interfaceDrink (IDrink in C#) with a price and a description. The espresso implements it and knows its own price. Milk implements it too, but keeps another drink in a field, one that is a Drink. When asked for the price, it asks the inner drink and adds €0.40.This is the Decorator pattern. It has three roles:
- the common shape: the interface that both the drinks and the wrappers have;
- the base: the espresso, which wraps nothing;
- the decorators: milk, syrup, the extra shot. Each keeps an inner drink, calls the same method on it and adds its own part.
Since the wrapper has the same shape as the drink, a wrapper can wrap a wrapper. Double syrup is two layers of syrup. The code that asks for the price doesn't know how many layers there are.
See it
Layer by layer
The common shape:
interface Drink { double cost(); String description();}The espresso and two add-ons. Every add-on gets the inner drink in its constructor:
class Espresso implements Drink { @Override public double cost() { return 2.00; }
@Override public String description() { return "Espresso"; }}
class Milk implements Drink { private final Drink inner;
public Milk(Drink inner) { this.inner = inner; }
@Override public double cost() { return inner.cost() + 0.40; }
@Override public String description() { return inner.description() + ", milk"; }}
class Syrup implements Drink { private final Drink inner;
public Syrup(Drink inner) { this.inner = inner; }
@Override public double cost() { return inner.cost() + 0.60; }
@Override public String description() { return inner.description() + ", syrup"; }}The order is put together from the inside out: first the espresso, then every layer wraps what there is so far.
Drink drink = new Espresso();drink = new Milk(drink);drink = new Syrup(drink);drink = new Syrup(drink);System.out.printf("%s: €%.2f%n", drink.description(), drink.cost());Espresso, milk, syrup, syrup: €3.60Drink drink = new Espresso();drink = new Milk(drink);drink = new Syrup(drink);drink = new Syrup(drink);System.out.printf("%s: €%.2f%n", drink.description(), drink.cost());Step by step, in words
- The espresso is the base. It wraps nothing and knows its price: 2.00.
- Milk wraps the espresso and keeps it in its field inner. drink now points to the milk, which is also a Drink.
- Syrup wraps the milk. The milk and the espresso inside are not touched.
- Syrup once more: a second layer of the same class. That is the double syrup.
- cost() goes to the outer layer. It adds nothing yet: first it calls inner.cost(), and so does every layer on the way in.
- The espresso answers 2.00. On the way out every layer adds its part to what came from inside.
- 3.60 comes out of the outer layer. description() goes the same way and collects the words.
The call to cost() goes in layer by layer down to the espresso, and the sum grows on the way out. None of the three classes knows about the others.
See it
A new layer without touching the old ones
The extra shot and the happy hour discount are two new classes. The discount doesn't add, it multiplies everything inside by 0.9:
class ExtraShot implements Drink { private final Drink inner;
public ExtraShot(Drink inner) { this.inner = inner; }
@Override public double cost() { return inner.cost() + 0.80; }
@Override public String description() { return inner.description() + ", extra shot"; }}
class Discount implements Drink { private final Drink inner;
public Discount(Drink inner) { this.inner = inner; }
@Override public double cost() { return inner.cost() * 0.9; }
@Override public String description() { return inner.description() + ", 10% off"; }}Drink order = new Discount(new ExtraShot(new Milk(new Espresso())));System.out.printf("%s: €%.2f%n", order.description(), order.cost());Espresso, milk, extra shot, 10% off: €2.88Espresso, Milk and Syrup didn't change. The new layer goes around any drink, including one that is already wrapped.
You read new Discount(new ExtraShot(new Milk(new Espresso()))) from the inside out: an espresso, wrapped in milk, wrapped in a shot, wrapped in a discount. The order matters: the discount applies only to what is inside it.
A wrapper around something that isn't a drink doesn't compile:
Drink drink = new Milk("Espresso");error: incompatible types: String cannot be converted to DrinkThe constructor wants a Drink, and the compiler stops before the first order.
See it
A wrapper around a method
The same idea works outside coffee too. The order service works, and now every call has to be visible: what was asked and what came back. The code of the service shouldn't be touched: it is tested, and the logs are temporary.
The service is behind an interface:
interface OrderService { int place(String drink, int quantity);}
class CafeOrderService implements OrderService { private int next = 1001;
@Override public int place(String drink, int quantity) { System.out.println("Preparing: " + quantity + " × " + drink); return next++; }}The wrapper has the same interface, logs and calls the real service:
class LoggingOrderService implements OrderService { private final OrderService inner;
public LoggingOrderService(OrderService inner) { this.inner = inner; }
@Override public int place(String drink, int quantity) { System.out.println("[log] place(" + drink + ", " + quantity + ")"); int number = inner.place(drink, quantity); System.out.println("[log] place returned " + number); return number; }}OrderService orders = new LoggingOrderService(new CafeOrderService());orders.place("Cappuccino", 2);orders.place("Espresso", 1);[log] place(Cappuccino, 2)Preparing: 2 × Cappuccino[log] place returned 1001[log] place(Espresso, 1)Preparing: 1 × Espresso[log] place returned 1002The code that takes orders gets an OrderService and doesn't know it is wrapped. You remove the logs by passing the service without the wrapper.
The same goes for measuring time, for a cache or for a retry after an error: each one is a separate layer.
More precisely, if you are curious
Java has decorators in its standard library: new BufferedReader(new InputStreamReader(System.in)) is an input stream wrapped in a stream that reads line by line. Spring puts wrappers around your classes to add transactions or a cache (@Transactional, @Cacheable).
Try it
Predict the output
The discount and the syrup are the same in both orders, only in a different order. What does the program print?
Drink a = new Discount(new Syrup(new Espresso()));Drink b = new Syrup(new Discount(new Espresso()));System.out.printf("€%.2f%n", a.cost());System.out.printf("€%.2f%n", b.cost());Try it
Find the bug
Someone rewrites the syrup:
class Syrup implements Drink { private final Drink inner;
public Syrup(Drink inner) { this.inner = inner; }
@Override public double cost() { return 0.60; }
@Override public String description() { return inner.description() + ", syrup"; }}Drink drink = new Syrup(new Milk(new Espresso()));System.out.printf("%s: €%.2f%n", drink.description(), drink.cost());Espresso, milk, syrup: €0.60The description is right, and the sum is €0.60. Which line is the bug on?
Pick the line with the bug.
The syrup returns only its own price and doesn't ask the inner drink. Everything under it (the espresso and the milk) disappears from the sum, without an error. The description is right, because description() does ask the inner drink, and that is exactly why the bug is easy to miss. A decorator has to call the same method on the object it wraps: return inner.cost() + 0.60;. Then the sum is €3.
The idea
When you don't need it
Decorator has a cost: a class for every layer, and the order is an object in an object in an object. It isn't worth it when:
- every add-on only adds a price and a name. Then the add-ons are data: a list of "name, price" pairs, and the sum is the price of the base plus the total of the list. A new syrup is a new row in the menu, not a new class;
- there is one layer and it is never removed. Its code can live in the class itself;
- the wrapped interface has twenty methods. Every wrapper has to forward all twenty, even when it changes only one.
Watch out for deep stacks too: six layers are six lines in a stack trace, and their order is visible only where they are put together.
It is worth it when the layers do different things (a discount, logging, a cache, a retry), are put together differently for different cases, and you don't want to touch what you wrap.
Exercise
Cappuccino with whipped cream
Solve the task in your editor.
Cappuccino with whipped cream. The coffee shop adds a cappuccino for €2.80 and whipped cream for €0.50. Write the classes Cappuccino and WhippedCream and order a cappuccino with whipped cream twice. The old classes are not touched.
Cappuccino, whipped cream, whipped cream: €3.80More tasks
- How long it takes. Write a
TimingOrderServicethat measures the time of everyplaceand prints how many milliseconds it took. Use it together with the logging. Which layer is on the outside, and why does it matter? - A large cup. A large cup costs 30% more. Add a layer
Large. Put it once around the espresso and once around the whole order with milk and syrup. Which one is fair to the customer? - A counter. A wrapper around the service counts how many orders went through and prints the count at the end. Think about where the counter lives: in the wrapper or in the service.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Interfaces · Exceptions · Lambdas · Design patterns
- Continues in: The shop: which pattern where · Caching
- Tasks on this topic: Design patterns