Decorator Pattern

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.

decorator pattern

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.