Using The Decorator Design Pattern in .NET

Posted by on . Last Updated on . Tagged:dotnet

The Decorator design pattern is a widely-used technique for extending the functionality of an existing object or component. By wrapping an object in a series of decorators, you can add new behaviours, features, or properties without modifying the underlying code. This makes it a powerful and flexible way to customize and extend software systems. In .NET, the Decorator design pattern can be used in various ways to enhance and modify existing classes and components, which can help you write more maintainable, extensible, and flexible code.

What Is The Decorator Design Pattern?

The idea behind the Decorator design pattern is to wrap the object you want to modify in a series of decorators, each adding a specific behaviour or feature to the object. In the Decorator design pattern, there are several key components:

  1. Component: This is the base object or interface you want to modify. It defines the basic behaviour or features of the object.
  2. Concrete Component: This is the concrete implementation of the component interface. It provides the base functionality of the object.
  3. Decorator: This is the abstract base class or interface for all decorators. It defines the basic structure of the decorators and how they should interact with the component.
  4. Concrete Decorator: This is the concrete implementation of the decorator interface. It provides the specific functionality or behaviour you want to add to the component.

The Decorator design pattern is often used when you need to add new functionality to an object but want to keep its underlying structure the same. This can be useful when you want to keep the existing codebase intact.

One of the benefits of using the Decorator design pattern is that it allows you to add new behaviours or features to an object without modifying the underlying code. This can make your code more maintainable and extensible, allowing you to add new functionality to an object without changing existing behaviour. By wrapping an object in a series of decorators, you can create complex and customizable objects tailored to your specific needs.

Implementing The Decorator Design Pattern In C#

There are two different ways of implementing the decorator pattern. Like the singleton design pattern, we can construct the pattern manually or with dependency injection. We’ll take a look at both, starting with manual creation:

  1. Define the Component interface or abstract class. This is the base object or interface that you want to modify. It defines the basic behaviour or features of the object:
public interface IComponent
{
    void Operation();
}
  1. Create the Concrete Component class. This is the concrete implementation of the component interface. It provides the base functionality of the object.
public class ConcreteComponent : IComponent
{
    public void Operation()
    {
        Console.WriteLine("ConcreteComponent.Operation()");
    }
}
  1. Create the Decorator abstract class. This is the abstract base class or interface for all decorators. It defines the basic structure of the decorators and how they should interact with the component.
public abstract class Decorator : IComponent
{
    protected IComponent _component;

    public Decorator(IComponent component)
    {
        _component = component;
    }

    public virtual void Operation()
    {
        _component.Operation();
    }
}
  1. Create the Concrete Decorator class. This is the concrete implementation of the decorator interface. It provides the specific functionality or behaviour that you want to add to the component.
public class ConcreteDecoratorA : Decorator
{
    public ConcreteDecoratorA(IComponent component) : base(component)
    {
    }

    public override void Operation()
    {
        base.Operation();
        Console.WriteLine("ConcreteDecoratorA.Operation()");
    }
}

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

    public override void Operation()
    {
        base.Operation();
        Console.WriteLine("ConcreteDecoratorB.Operation()");
    }
}
  1. Implement the Decorator design pattern by wrapping components in decorators.
IComponent component = new ConcreteComponent();
component = new ConcreteDecoratorA(component);
component = new ConcreteDecoratorB(component);

component.Operation();

When the above code is run, we end up with the following output:

ConcreteComponent.Operation()
ConcreteDecoratorA.Operation()
ConcreteDecoratorB.Operation()

The order of the output can easily be changed by changing the implementation of the decorators. Note that by using interfaces and abstract classes, we can create a flexible and extensible system that can be easily customized and modified.

The Decorator Design Pattern & Dependency Injection

The Decorator design pattern can also easily be used with dependency injection frameworks using NuGet packages like Scrutor. If we use the classes/interfaces that we’ve previously declared, we can add dependency injection as follows:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection()
                    .AddScoped<IComponent, ConcreteComponent>()
                    .Decorate<IComponent, ConcreteDecoratorA>()
                    .Decorate<IComponent, ConcreteDecoratorB>()
                    .BuildServiceProvider();

services.GetRequiredService<IComponent>().Operation();

// ...component definitions below...

This helps prevent the need for manual construction of objects which could reduce a large amount of repetitive code when the object being decorated has a lot of dependencies. The above code produces this output:

ConcreteComponent.Operation()
ConcreteDecoratorA.Operation()
ConcreteDecoratorB.Operation()

Frequently Asked Questions About The Decorator Design Pattern

What Are Some Real-World Usages Of The Decorator Design Pattern?

  • Adding logging or error handling to a database access component
  • Adding caching or throttling to a web service client
  • Adding encryption or compression to a file storage component
  • Adding formatting or validation to a user input component

Can The Decorator Design Pattern Be Used To Modify Existing Behaviour Of An Object?

No, the Decorator pattern is designed to add new behaviours or features to an object, not to remove or modify existing behaviour. If you need to change existing behaviour in an object, consider other patterns like the Strategy pattern or the Template Method pattern.

How Does The Decorator Design Pattern Differ From Inheritance?

Inheritance is another way to add new behaviours or features to an object, but it differs from the Decorator pattern in several ways. Inheritance is a static relationship between classes, whereas the Decorator pattern is a dynamic relationship between objects. Inheritance can lead to a problematic class hierarchy to maintain and extend, whereas the Decorator pattern allows for more flexible and modular code.

Stuart Blackler is a seasoned technologist with over 15 years of commercial experience in the .NET ecosystem. Holding a degree in Computer Science, Stuart has earned certifications as a C# developer through Microsoft and as an AWS Solutions Architect and Developer. Stuart is the creator of the popular YouTube channel CodeWithStu, where he delves into topics close to his heart, including .NET, AWS, DevOps, and software architecture with a commitment to sharing knowledge and fostering a community of learners.

More in the 'Design Patterns In Dotnet' series:

  1. Using The Factory Design Pattern in .NET
  2. Using The Decorator Design Pattern in .NET (This article)
  3. Using The Singleton Design Pattern in .NET