Hello everyone, and welcome to .NET Core Central. In this blog post, I am going to walk through the Decorator Pattern.
The decorator design pattern is one of the patterns from the gang of four design patterns. The decorator pattern is a structural design pattern.
The main intent of the pattern is to attach additional responsibility to an object dynamically. An alternative to subclassing.
Decorator Pattern Usecase
So let us walk through a problem statement and see what exactly the decorator pattern helps us do. And also I am going to discuss my take on this pattern and do I find this pattern interesting and useful in my day-to-day work.
So the case that we are going to talk about is that of a cake. So let us say we have a bakery and in the bakery, the baker makes the cake.
Now when we talk about cake, for that purpose let us create an interface called ICake
. Because cakes can be of different types. There can be chocolate cake, there can be vanilla cake, and so on and so forth.
namespace DecoratorPattern.Demo;
public interface ICake
{
void AddLayer(string layer);
void PrintLayers();
}
And let us say the ICake
is going to have an AddLayer
method, to add layers of the cake and it takes a parameter named layer
of type string
. And then it has another method PrintLayers
. These are the two main methods of the interface ICake
.
Cake Implementations
Bow what we will do is, we will create a VanillaCake
class. The VanillaCake
class will implement the interface ICake
.
Inside the VanillaCake
class will declare variable layers
of type List<string>
. The variable layers
is where all the layers of the cake will be added.
The AddLayers
method will add the incoming layer
string
to the layers
variable. And for the PrintLayer
method implementation, we can loop through the layers
and print the layer information to the console.
Vanilla cake class
namespace DecoratorPattern.Demo;
public class VanillaCake : ICake
{
private readonly List<string> layers = new();
public void AddLayer(string layer)
{
layers.Add(layer);
}
public void PrintLayers()
{
foreach (var layer in layers)
{
Console.WriteLine($"Layer: {layer}");
Console.WriteLine(" ---------- ");
}
}
}
Chocolate cake class
So now that we have the VanillaCake
class, very similar to this class we can have a ChocolateCake
class as well. Hence let us go ahead and create a new class and we will name it as ChocolateCake
.
And for the ChocolateCake
class, we can have a similar implementation as the VanillaCake
class. A little bit of change for the console printout, where we will add the word chocolate.
namespace DecoratorPattern.Demo;
public class ChocolateCake : ICake
{
private readonly List<string> layers = new();
public void AddLayer(string layer)
{
layers.Add(layer);
}
public void PrintLayers()
{
foreach (var layer in layers)
{
Console.WriteLine($"Chocolate Layer: {layer}");
Console.WriteLine(" ---------- ");
}
}
}
This is the implementation, and it is a very simple implementation to demonstrate the core concept. We have two different types of cake one is vanilla and one is chocolate.
New requirement (case for decorator pattern)
And after the baker starts selling the cakes, he started getting new requirements. So his new requirement is someone came in and said they want to print their name on the cake.
One way to implement this requirement is using a new class called ChocolateCakeWithName
. And this class can derive from the ChocolateCake
class. And we can add a name to this new class.
Now the exact same implementation needs to happen for the vanilla cake as well. So now we have to create a VanillaCakeWithName
class and derived it from the VanillaCake
class to add features.
And as you can see, very quickly the implementation will get really messy as new requirements keep coming from the clients.
And this is where the decorator pattern will come in handy. Just like the name, the decorator pattern is going to decorate the object.
The decorator interface
To implement the decorator design pattern to solve the new requirement, we can create a new interface. We will name the new interface as ICakeMessageDecorator
, and this interface is going to have a single method Decorate
. And the Decorate
method is going to take a string
parameter message
.
namespace DecoratorPattern.Demo;
public interface ICakeMessageDecorator
{
void Decorate(string message);
}
The Decorate
method takes the incoming message and decorates the cake with this message. For a different scenario, it can be an instruction because the decoration can be printing a name or it can be doing something else. So the interface can be generic enough or it can be a specific interface.
CakeMessageDecorator class
Now we are going to create a new class CakeMessageDecorator
and this class is going to implement the interface ICakeMessageDecorator
.
namespace DecoratorPattern.Demo;
public class CakeMessageDecorator
: ICakeMessageDecorator
{
private readonly ICake cake;
public CakeMessageDecorator(ICake cake)
{
this.cake = cake;
}
public void Decorate(string message)
{
cake.AddLayer($"Message for the cake: {message}");
}
}
The CakeMessageDecorator
will have a constructor, and the interface ICake
will be the only parameter to the constructor. And in the Decorate
method, we are going to use the instance of the ICake
and call AddLayer
to add the incoming message.
For the message, we will make it a little bit different for the decorator.
As you can see with the cake decorator now we can use both chocolate cake as well as vanilla cake to decorate it with the message on top of the cake. Without using the decorator pattern you will have to derive from both the classes and create their subclasses to achieve the same functionality.
As you can see decorator pattern simplifies how you decorate an object and gives a very simple alternative for subclassing. Instead of using the subclass, we can use a single class that can decorate an object based on a particular interface. So that is the main advantage of using a decorator pattern.
Wiring things up
Firstly, we will register all the services in the dependency injection container. And for that, I am going to update the Program
class.
builder.Services.AddSingleton<ICake, ChocolateCake>();
builder.Services.AddSingleton<ICakeMessageDecorator, CakeMessageDecorator>();
Secondly, I will create a new minimal API for testing this, and I will name it as decorate
. To this API I will inject ICakeMessageDecorator
and ICake
.
app.MapGet("/decorate", (
ICakeMessageDecorator cakeMessageDecorator,
ICake cake) =>
{
cakeMessageDecorator.Decorate("Happy Birthday!");
cake.PrintLayers();
})
.WithName("Decorate");
Finally, inside the API, I will call Decorate
on the ICakeMessageDecorator
instance passing a message. Finally, I will call PrintLayers
on the ICake
instance.
The complete Program
class is below:
using DecoratorPattern.Demo;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<ICake, ChocolateCake>();
builder.Services.AddSingleton<ICakeMessageDecorator, CakeMessageDecorator>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/decorate", (
ICakeMessageDecorator cakeMessageDecorator,
ICake cake) =>
{
cakeMessageDecorator.Decorate("Happy Birthday!");
cake.PrintLayers();
})
.WithName("Decorate");
app.Run();
Running the application
Now, if I run the application and execute the API from the swagger, I can see the output printing as expected.
Conclusion
I personally think it is a very useful pattern because it reduces the number of codes you might have to write otherwise. Now the question is how it compares with the SOLID design principles.
Well, to be honest, if we start using interface segregation and the single responsibility principle from SLOID, we will end up using a decorator pattern for a situation like this. But knowing this design pattern and using it makes life simpler. Otherwise, we will stumble upon the decorator pattern when you are trying to solve a problem of this nature.
A Youtube video for this implementation is here.