The decorator pattern is a structural design pattern that give us the ability to enhance the functionality of existing objects. This is done by surrounding the objects with a special kind of class known as a decorator class which can add new behavior to the object at runtime.
Problem
Consider a scenario where you have to create a software application for a coffee shop. The application allows the user to order coffee drinks and customize them with various toppings. One way to solve this problem is to use simple condition statements to check what toppings the customer wants. This approach is simple and easy to understand but as the number of toppings grows the conditional statements can become complex and difficult to maintain.
Solution
The decorator pattern is an excellent way to tackle such a problem because we can add new functionality without modifying the existing code, making it a more flexible solution as the number of toppings grow.
Decorator pattern consists of the following parts:
Component: This is the interface that defines the methods that the objects being decorated should implement.
Concrete Component: This is the class that implements the implements the Component interface
Decorator: This is an abstract class that implements the Component interface. It also holds a reference to an object of the Component type.
Concrete Decorator: This is the class that extends the Decorator class and adds new behavior to the objects.
Client: Can wrap components in multiple layers of decorators, this is the class that uses the decorator pattern.
Coffee shop code example
// This is our component interface
public interface ICoffee
{
string GetDescription();
decimal GetCost();
}
Next step is to create a concrete class that implements our component interface. Lets call our class Espresso, since we as developers like to drink a lot if it đ :
// This is our concrete component class
public sealed class Espresso : ICoffee
{
public string GetDescription()
{
return "Espresso";
}
public decimal GetCost()
{
return 1.99m;
}
}
Then, we can create a decorator for example ToppingDecorator
that implements the ICoffee
interface and contains a reference to an ICoffee
object. Decorators can be abstract class or interface:
// This is our decorator abstract class
public abstract class ToppingDecorator : ICoffee
{
protected readonly ICoffee Coffee;
protected ToppingDecorator(ICoffee coffee)
{
Coffee = coffee;
}
public virtual string GetDescription()
{
return Coffee.GetDescription();
}
public virtual decimal GetCost()
{
return Coffee.GetCost();
}
}
Now we can finally create a concrete decorator class that will add behavior to our espresso and give some extra flavor! On our example we will create two decorator classes one named WhippedCream
and the other ChocolateSyrup
:
// This is our first concrete decorator class
public sealed class WhippedCream : ToppingDecorator
{
public WhippedCream(ICoffee coffee) : base(coffee)
{
}
public override string GetDescription()
{
return Coffee.GetDescription() + ", Whipped Cream";
}
public override decimal GetCost()
{
return Coffee.GetCost() + 0.5m;
}
}
// This is our second concrete decorator class
public sealed class ChocolateSyrup : ToppingDecorator
{
public ChocolateSyrup(ICoffee coffee) : base(coffee)
{
}
public override string GetDescription()
{
return Coffee.GetDescription() + ", Chocolate Syrup";
}
public override decimal GetCost()
{
return Coffee.GetCost() + 0.75m;
}
}
Last but not least we can use the decorator classes to create and customize coffee drinks in our client:
// This is our client
ICoffee espresso = new Espresso();
Console.WriteLine(espresso.GetDescription() +
": $" + espresso.GetCost());
// Add whipped cream and chocolate syrup to the espresso
ICoffee espressoWithToppings = new WhippedCream(
new ChocolateSyrup(espresso));
console.WriteLine(espressoWithToppings.GetDescription()
+ ": $" + espressoWithToppings.GetCost());
The output of the above should be the following:
Espresso: $1,99
Espresso, Chocolate Syrup, Whipped Cream: $3,24
When to use the decorator pattern
We should use decorator pattern when we want to add new behavior to an individual object or a group of related objects without affecting the behavior of other objects of the same class.
Pros and cons of using the decorator pattern
âď¸ Allow us to add new behavior to existing objects dynamically
âď¸ Make it easy to create different combinations of behavior
âď¸ Making it easier to change the objects without affecting the client code
â Increases the complexity of the code
â It may make the code less straightforward to read, as it adds an additional level of complexity.