Skip to content
wannacode

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 virtual decimal Cost() => 2.00m;
public virtual string Description() => "Espresso";
}
class EspressoWithMilk : Espresso
{
public override decimal Cost() => base.Cost() + 0.40m;
public override string Description() => base.Description() + ", milk";
}
class EspressoWithMilkAndSyrup : EspressoWithMilk
{
public override decimal Cost() => base.Cost() + 0.60m;
public override string Description() => base.Description() + ", syrup";
}
Espresso drink = new Espresso();
Console.WriteLine($"{drink.Description()}: €{drink.Cost():F2}");
drink = new EspressoWithMilk();
Console.WriteLine($"{drink.Description()}: €{drink.Cost():F2}");
drink = new EspressoWithMilkAndSyrup();
Console.WriteLine($"{drink.Description()}: €{drink.Cost():F2}");
Output
Espresso: €2.00
Espresso, milk: €2.40
Espresso, milk, syrup: €3.00

It 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 interface Drink (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 IDrink
{
decimal Cost();
string Description();
}

The espresso and two add-ons. Every add-on gets the inner drink in its constructor:

class Espresso : IDrink
{
public decimal Cost() => 2.00m;
public string Description() => "Espresso";
}
class Milk : IDrink
{
private readonly IDrink inner;
public Milk(IDrink inner)
{
this.inner = inner;
}
public decimal Cost() => inner.Cost() + 0.40m;
public string Description() => inner.Description() + ", milk";
}
class Syrup : IDrink
{
private readonly IDrink inner;
public Syrup(IDrink inner)
{
this.inner = inner;
}
public decimal Cost() => inner.Cost() + 0.60m;
public string Description() => inner.Description() + ", syrup";
}

The order is put together from the inside out: first the espresso, then every layer wraps what there is so far.

IDrink drink = new Espresso();
drink = new Milk(drink);
drink = new Syrup(drink);
drink = new Syrup(drink);
Console.WriteLine($"{drink.Description()}: €{drink.Cost():F2}");
Output
Espresso, milk, syrup, syrup: €3.60

IDrink drink = new Espresso();
drink = new Milk(drink);
drink = new Syrup(drink);
drink = new Syrup(drink);
Console.WriteLine($"{drink.Description()}: €{drink.Cost():F2}");
Step by step, in words
  1. The espresso is the base. It wraps nothing and knows its price: 2.00.
  2. Milk wraps the espresso and keeps it in its field inner. drink now points to the milk, which is also a IDrink.
  3. Syrup wraps the milk. The milk and the espresso inside are not touched.
  4. Syrup once more: a second layer of the same class. That is the double syrup.
  5. Cost() goes to the outer layer. It adds nothing yet: first it calls inner.Cost(), and so does every layer on the way in.
  6. The espresso answers 2.00. On the way out every layer adds its part to what came from inside.
  7. 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 : IDrink
{
private readonly IDrink inner;
public ExtraShot(IDrink inner)
{
this.inner = inner;
}
public decimal Cost() => inner.Cost() + 0.80m;
public string Description() => inner.Description() + ", extra shot";
}
class Discount : IDrink
{
private readonly IDrink inner;
public Discount(IDrink inner)
{
this.inner = inner;
}
public decimal Cost() => inner.Cost() * 0.9m;
public string Description() => inner.Description() + ", 10% off";
}
IDrink order = new Discount(new ExtraShot(new Milk(new Espresso())));
Console.WriteLine($"{order.Description()}: €{order.Cost():F2}");
Output
Espresso, milk, extra shot, 10% off: €2.88

Espresso, 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:

IDrink drink = new Milk("Espresso");
Error
error CS1503: Argument 1: cannot convert from 'string' to 'IDrink'

The constructor wants a IDrink, 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 IOrderService
{
int Place(string drink, int quantity);
}
class CafeOrderService : IOrderService
{
private int next = 1001;
public int Place(string drink, int quantity)
{
Console.WriteLine($"Preparing: {quantity} × {drink}");
return next++;
}
}

The wrapper has the same interface, logs and calls the real service:

class LoggingOrderService : IOrderService
{
private readonly IOrderService inner;
public LoggingOrderService(IOrderService inner)
{
this.inner = inner;
}
public int Place(string drink, int quantity)
{
Console.WriteLine($"[log] Place({drink}, {quantity})");
int number = inner.Place(drink, quantity);
Console.WriteLine($"[log] Place returned {number}");
return number;
}
}
IOrderService orders = new LoggingOrderService(new CafeOrderService());
orders.Place("Cappuccino", 2);
orders.Place("Espresso", 1);
Output
[log] Place(Cappuccino, 2)
Preparing: 2 × Cappuccino
[log] Place returned 1001
[log] Place(Espresso, 1)
Preparing: 1 × Espresso
[log] Place returned 1002

The code that takes orders gets an IOrderService 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

.NET has decorators in its own library: GZipStream and BufferedStream wrap another Stream and use it from the inside. In ASP.NET Core every middleware gets the request, does its part and calls the next one, and a DelegatingHandler wraps the handler that HttpClient sends its requests through.

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?

IDrink a = new Discount(new Syrup(new Espresso()));
IDrink b = new Syrup(new Discount(new Espresso()));
Console.WriteLine($"€{a.Cost():F2}");
Console.WriteLine($"€{b.Cost():F2}");
Answer

Try it

Find the bug

Someone rewrites the syrup:

class Syrup : IDrink
{
private readonly IDrink inner;
public Syrup(IDrink inner)
{
this.inner = inner;
}
public decimal Cost() => 0.60m;
public string Description() => inner.Description() + ", syrup";
}
IDrink drink = new Syrup(new Milk(new Espresso()));
Console.WriteLine($"{drink.Description()}: €{drink.Cost():F2}");
Output
Espresso, milk, syrup: €0.60

The description is right, and the sum is €0.60. Which line is the bug on?

Pick the line with the bug.

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.

Output
Cappuccino, whipped cream, whipped cream: €3.80

More tasks

  • How long it takes. Write a TimingOrderService that measures the time of every place and 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

1. A customer wants an espresso with milk and syrup twice. What do you write with Decorator?
2. What does Milk do when asked for cost()?
3. LoggingOrderService wraps CafeOrderService. What changes in CafeOrderService to get logs?
4. When don't you need Decorator?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·