- Published on
Design Patterns: Decorator
- Authors
- Name
- Ivan Gechev
The Decorator design pattern allows us to add new functionality to an object dynamically without affecting its behavior. It is a structural pattern that uses composition to add new features to an object at runtime.
The basic idea of the Decorator design pattern is to wrap an object with one or more decorators. Each decorator adds new behavior to the original object. This creates a chain of objects, where each object adds new functionality to the previous one.
Next, we'll take a look at the code example, using one of our favorite beverages - coffee, to see how the Decorator design pattern works in practice.
Understanding the Decorator Design Pattern
The code we will use in this example is written in C# and consists of the ICoffee
interface as well as several classes that implement it: Espresso
, DoubleEspresso
, Latte
, and Cappuccino
.
We start with the interface:
public interface ICoffee
{
decimal CalculatePrice();
string GetDescription();
}
In the ICoffee
interface we define two methods: CalculatePrice
and GetDescription
. They will be used to calculate the price of the coffee and get its description.
Next, we define our first coffee:
public class Espresso : ICoffee
{
public decimal CalculatePrice() => 2.5M;
public string GetDescription() => "Espresso";
}
The Espresso
class is a concrete implementation of the ICoffee
interface. It provides an implementation of the CalculatePrice
and GetDescription
methods.
Now, that we have a class to decorate, we can start implementing the Decorator design pattern. The DoubleEspresso
, Latte
, and Cappuccino
classes are going to act as our decorators. They will add new behavior to an existing ICoffee
object - in our case, this will be an instance of the Espresso
class. Each decorator class will take a parameter of the ICoffee
type in its constructor parameter and will wrap it to add new features:
public class DoubleEspresso : ICoffee
{
private readonly ICoffee _baseCoffee;
public DoubleEspresso(ICoffee baseCoffee)
{
_baseCoffee = baseCoffee ??
throw new ArgumentNullException(
nameof(baseCoffee),
"Base coffee must not be null!");
}
public decimal CalculatePrice()
{
return _baseCoffee.CalculatePrice() + 1;
}
public string GetDescription()
{
return "Double " + _baseCoffee.GetDescription();
}
}
The DoubleEspresso
decorator class doubles the price of the base coffee and adds the word "Double" to its description.
public class Latte : ICoffee
{
private readonly ICoffee _baseCoffee;
public Latte(ICoffee baseCoffee)
{
_baseCoffee = baseCoffee ??
throw new ArgumentNullException(
nameof(baseCoffee),
"Base coffee must not be null!");
}
public decimal CalculatePrice()
{
return _baseCoffee.CalculatePrice() * 1.5M;
}
public string GetDescription()
{
return _baseCoffee.GetDescription() + " with steamed milk";
}
}
A Latte
is just an Espresso
with steamed milk, so we will add that to the description of our base coffee. As for the price - we increase it by 50%.
public class Cappuccino : ICoffee
{
private readonly ICoffee _baseCoffee;
public Cappuccino(ICoffee baseCoffee)
{
_baseCoffee = baseCoffee ??
throw new ArgumentNullException(
nameof(baseCoffee),
"Base coffee must not be null!");
}
public decimal CalculatePrice()
{
return _baseCoffee.CalculatePrice() * 2M;
}
public string GetDescription()
{
return _baseCoffee.GetDescription() + " with steamed milk foam";
}
}
The Cappuccino
decorator adds steamed milk foam to the base coffee's description and doubles its price.
Now that we have everything ready, let's start using the Decorator pattern!
Using the Decorator Design Pattern
After we have set things up in the previous section, we go to our Program.cs
file. In its Main
method, we create four ICoffee
objects:
static void Main(string[] args)
{
var espresso = new Espresso();
var doubleEspresso = new DoubleEspresso(espresso);
var latte = new Latte(espresso);
var cappuccino = new Cappuccino(espresso);
Console.WriteLine($"{espresso.GetDescription()}: {espresso.CalculatePrice()}");
Console.WriteLine($"{doubleEspresso.GetDescription()}: {doubleEspresso.CalculatePrice()}");
Console.WriteLine($"{latte.GetDescription()}: {latte.CalculatePrice()}");
Console.WriteLine($"{cappuccino.GetDescription()}: {cappuccino.CalculatePrice()}");
Console.WriteLine($"{espresso.GetDescription()}: {espresso.CalculatePrice()}");
}
We start with an Espresso
, which will serve as our base type of coffee. Then we create a DoubleEspresso
, a Latte
, and a Cappuccino
. All of them are wrapped around our initial Espresso
instance.
Finally, we print out our coffees on the console, starting and finishing with the Espresso
:
Espresso: 2.5
Double Espresso: 3.5
Espresso with steamed milk: 3.75
Espresso with steamed milk foam: 5.0
Espresso: 2.5
From the output, we can see that everything is working as expected. We can also see that the original Espresso
instance is not modified even though we have "decorated" it three times.
Conclusion
One of the main advantages of the Decorator design pattern is that it enables us to have a high degree of flexibility when adding new functionality to an object at runtime. We achieve this without modifying the underlying code. This can be especially useful in situations where we need to add new features to a class but don't have access to its source code. Or it can be valuable in cases where we want to avoid making changes that could break existing code.
Another benefit of the Decorator design pattern is that it can help to reduce code duplication, as it allows us to reuse existing code while adding new functionality to it.