C# Decorator Pattern: Why It’s Still Useful in 2023


Many people have a love/hate relationship with their code. Some find it too stressful to change it, while others change it freely and make methods and classes bigger and uglier.

Do you want to know how to add extra features to your classes without modifying them?

Use the Decorator pattern.

The Decorator pattern is a design pattern that allows you to add behavior to an existing class without modifying the original code. This is done by creating a new class that wraps the original class and adds the desired behavior.

This is a powerful pattern that you can use for various purposes, such as adding security, logging, or additional responsibility.

This article goes through some practical examples of implementing the decorator pattern in C# and shows how it can help you achieve better results.

What is the Decorator design pattern in C#? 

The Decorator is a structural design pattern in C# that allows an object to be dynamically enhanced with new functionality without having to be changed. This is done using a decorator, an object that wraps another object. The decorator class implements the same interface as the object it wraps.

The Decorator pattern allows you to extend the object’s behavior at runtime. It is also an excellent alternative to subclassing to extend functionality.

The decorator object usually performs additional work before or after delegating the call to the wrapped object.

The Decorator pattern is one of the many design patterns that are part of the design patterns catalog by the “Gang of Four”, published in the book “Design Patterns: Elements of Reusable Object-Oriented Software” in 1994.

How the Decorator design pattern works

The Decorator design pattern works by having a decorator object that wraps the original class. The decorator object contains new or changed functionality.

The following diagram shows how the decorator pattern works.

  1. The request comes in and the decorator class responds to it.
  2. The decorator class does its functionality before or after passing the work to the wrapped class.
  3. It then forwards the call to the original class that should handle the request.
  4. The wrapped class does its usual job, unaware of the decorator class.
  5. The wrapped class finishes the job and returns the result to the decorator. The decorator returns the result to the original caller.

Some scenarios where you could use this pattern are adding logging or caching functionality to any object.

Decorator Pattern – the participants

In its base implementation, the Decorator pattern has four participants:

  • Component – this is an interface that your class (ConcreteComponent) implements. The implementing class will be decorated with the decorator pattern.
  • ConcreteComponent – the main class that performs some operation. This is the class you want to extend.
  • Decorator – base decorator implementation. Takes an instance of a Component interface, most commonly through the constructor.
  • ConcreteDecorator – This is the class that inherits from the Decorator class. It adds additional behavior.

The following diagram is a class diagram of the Decorator pattern.

decorator pattern class diagram

Here is how to implement the sample code for the Decorator pattern in C#.

Start by adding a base interface that your wrapped class will implement.

interface IComponent
{
    void Operation();
}

Next, implement the interface. The ConcreteComponent is the class you will extend later with the decorator.

class ConcreteComponent : IComponent
{
    public void Operation()
    {
        Console.WriteLine("ConcreteComponent operation");
    }
}

Next, create a base Decorator implementation. It takes IComponent as a constructor parameter.

abstract class Decorator : IComponent
{
    protected IComponent component;

    public Decorator(IComponent component)
    {
        this.component = component;
    }

    public virtual void Operation()
    {
        component.Operation();
    }
}

Finally, inherit the Decorator class. 

class ConcreteDecoratorA : Decorator
{
    public ConcreteDecoratorA(IComponent component) : base(component)
    {
    }

    public override void Operation()
    {
        AddedBehavior();
        base.Operation();
    }

    private void AddedBehavior()
    {
        Console.WriteLine("ConcreteDecoratorA AddedBehavior");
    }
}

class ConcreteDecoratorB : Decorator
{
    public ConcreteDecoratorB(IComponent component) : base(component)
    {
    }

    public override void Operation()
    {
        base.Operation();
        AddedBehavior();
    }

    private void AddedBehavior()
    {
        Console.WriteLine("ConcreteDecoratorB AddedBehavior");
    }
}

Note that you can skip the abstract class if you don’t need to share common implementation between decorators.

Finally, arrange the decoration.

var main = new ConcreteComponent();
var decoratorA = new ConcreteDecoratorA(main);
var decoratorB = new ConcreteDecoratorB(decoratorA);

decoratorB.Operation();

You get the following output when you run the above code in the console application.

decorator pattern - sample implementation output

The full source code:

interface IComponent
{
    void Operation();
}

class ConcreteComponent : IComponent
{
    public void Operation()
    {
        Console.WriteLine("ConcreteComponent operation");
    }
}

abstract class Decorator : IComponent
{
    protected IComponent component;

    public Decorator(IComponent component)
    {
        this.component = component;
    }

    public virtual void Operation()
    {
        component.Operation();
    }
}

class ConcreteDecoratorA : Decorator
{
    public ConcreteDecoratorA(IComponent component) : base(component)
    {
    }

    public override void Operation()
    {
        AddedBehavior();
        base.Operation();
    }

    private void AddedBehavior()
    {
        Console.WriteLine("ConcreteDecoratorA AddedBehavior");
    }
}

class ConcreteDecoratorB : Decorator
{
    public ConcreteDecoratorB(IComponent component) : base(component)
    {
    }

    public override void Operation()
    {
        base.Operation();
        AddedBehavior();
    }

    private void AddedBehavior()
    {
        Console.WriteLine("ConcreteDecoratorB AddedBehavior");
    }
}

Decorator pattern – The real-world code example in C#

Let’s see one real-world example of using the decorator pattern.

You will implement a simple repository pattern and use a decorator to add logging functionality.

A repository pattern is an architectural pattern that defines a layer between the business model and the data access logic. It is used to separate the data access logic from the business logic.

Start by creating a repository pattern for the Order record:

record Order(int OrderId, decimal TotalAmount);


internal interface IOrderRepository
{
    void Add(Order order);
    void Update(Order order);
    void Delete(Order order);
    Order Get(int orderId);
}

class OrderRepository : IOrderRepository
{
    public void Add(Order order)
    {
        Console.WriteLine("Original Repo: Order added");
    }

    public void Update(Order order)
    {
        Console.WriteLine("Original Repo: Order updated");
    }
    public void Delete(Order order)
    {
        Console.WriteLine("Original Repo: Order deleted");
    }

    public Order Get(int orderId)
    {
        Console.WriteLine("Original Repo: Order retrieved");
        return new Order(orderId, 100);
    }
}

Next, add the LoggerRepository. The LoggerRepository will delegate all method calls to the decorated object while also logging information about the method call.

class LoggerRepository : IOrderRepository
{
    private readonly IOrderRepository _orderRepository;

    public LoggerRepository(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public void Add(Order order)
    {
        Console.WriteLine("Logger: Order added");
        _orderRepository.Add(order);
    }

    public void Update(Order order)
    {
        Console.WriteLine("Logger: Order updated");
        _orderRepository.Update(order);
    }

    public void Delete(Order order)
    {
        Console.WriteLine("Logger: Order deleted");
        _orderRepository.Delete(order);
    }

    public Order Get(int orderId)
    {
        Console.WriteLine("Logger: Order retrieved");
        return _orderRepository.Get(orderId);
    }
}

when we chain the classes:

new LoggerRepository(new OrderRepository()).Add(new Order(1, 120));

The console shows the following output.

That means you have successfully added logging to the OrderRepository class.

When to use the Decorator design pattern?

The Decorator design pattern is valuable for adding behavior to an object without changing the original object or subclassing. This can be useful when you need to add behavior to an object that you don’t have control over or when you need to add behavior dynamically.

Another use case is when you have to make changes to the class, but it’s awkward to modify it.

How to recognize the Decorator design pattern?

You will recognize the decorator design pattern by classes that implement an interface but take the same interface as the constructor parameter. The decorator can then be passed to other objects needing added functionality.

The pattern uses delegation to wrap an object with a decorator object that adds the new functionality.

What are the advantages of the Decorator design pattern?

The advantages of the Decorator design pattern are:

  • Add new functionality to an existing object without changing the original code.
  • It’s a versatile pattern. You can use it in a wide variety of situations.
  • Create complex designs without having to use inheritance.
  • Easy to understand and implement.

All in all, it’s a perfect design pattern. It’s still relevant in modern applications.

What are the disadvantages of the decorator design pattern?

The disadvantages of the decorator pattern are:

  • It can be complicated to use the decorator pattern when the implementing interface is big
  • It can be complicated to configure the decorator stack
  • Added complexity makes debugging harder.

Real projects that use the decorator pattern and its relevancy in 2023

In ASP.Net Core (or .NET 6), the decorator pattern is used in middleware to provide extra functionality to existing middleware components. 

Middleware is a modular component that you can plug into the ASP.Net Core (or simply called .NET 6) pipeline to handle requests and responses. Developers can easily create and add new middleware components to the ASP.Net Core pipeline by using the decorator pattern.

For example, you can use the UseMiddleware method to add extra functionality to an existing middleware component. This allows for a more flexible and extensible middleware architecture.

That’s why this structural pattern is still a valuable and relevant tool for developers in 2023.

What are related patterns to the decorator design pattern?

There are three related design patterns to the Decorator pattern:

  1. Adapter pattern
  2. Composite pattern
  3. Strategy pattern

The Adapter pattern is used to convert the interface of one class into another. It’s different from the Decorator pattern since the Decorator pattern only changes responsibilities and doesn’t change the object’s interface.

The Composite pattern helps to build structures made up of individual objects. The Decorator pattern is different since it’s not used for object aggregation.

The Strategy pattern is used to select an algorithm at runtime based on the program’s specific needs.

All three of these patterns are similar to the Decorator pattern in that they help to improve the flexibility and extendibility of code. By using these patterns, software developers can avoid hard-coding specific behavior into a program, making it easier to change how the program behaves in the future.

As a result, these design patterns can be beneficial when building large and complex software applications.

Conclusion

The Decorator design pattern is a powerful tool for creating flexible and extensible software.

By wrapping a piece of functionality in a decorator, you can add new behavior to an existing object without having to change the original object. This can be helpful when you need to add new behavior to third-party objects that you cannot modify. The decorator design pattern is also helpful in creating clean and well-organized code.

Decorators can make your code more readable and easier to maintain when used correctly.

If you want to learn more about design patterns, check the separate in-depth article about design patterns in C#.

FAQ

faq

Is abstract class required in the decorator pattern?

No, an abstract class is not required in the decorator pattern. But even if an abstract class is not required in the decorator pattern, you can use it to provide a common implementation for different decorator implementations.

Do the various decorators and the base behavior share a common interface?

Yes. Decorators can share a common interface with the base behavior.

Why should we use the decorator pattern?

The decorator pattern is a helpful way to add functionality to an object without changing the object itself. In addition, you can use the decorator pattern to add or remove functionality from an object at runtime dynamically.

What is a function decorator?

A function decorator is a function that takes another function as an argument and extends the behavior of the latter function without explicitly modifying it.

What happens if I add a new method to the interface that the decorator implements?

If you add a new method to the interface that the decorator implements, the decorator will need to be updated to include the new method.

What are the usual uses of a decorator pattern?

There are many possible uses for the decorator pattern. For example, you can use a decorator to add additional responsibilities to an object, such as security, logging, or caching. A decorator alters the behavior of an object without changing the existing functionality.

Recent Posts