Skip to content

Design Patterns: Decorator

Kaisinel edited this page Feb 13, 2021 · 7 revisions

Decorator

Problem

Have you ever had a need to stack behaviour to an existing implementation? For example, imagine there are 3 behaviours: A, B, C with the same definitions. You need to do a mix of them: either A or B or C or AB or AC or BC or ABC. In fact, the order itself matters too.We might seem like a nice solution to this problem. We could have a class C, inheriting class B, inheriting class A, overriding the same method and adding behaviour on top. But, when we need to make a slightly different combo (A with C, but not with B)- we will fail miserably with inheritance because we will end up with dozens of classes.

But there is another, better way.

Decorator

Decorator is a structural design pattern which aims to add additional behaviour to an existing class without modifying it. The most basic decorator looks like this:

// To be decorated.
public interface ILogger
{
  void Log(string text);
}

public class ConsoleLogger : ILogger
{
  public void Log()
  {
    // Log to console.
  }
}

// Decorator
public class LoggerToDb: ILogger
{
  // Decoratee = original interface implementations + other decorators.
  private readonly ILogger _logger;
  public LoggerToDb(ILogger logger)
  {
    _logger = logger;
  }

  public void Log(string text)
  {
    // We do the same as the decorated...
    _logger.Log(text);
    // ... and a little more.
    LogToDb(text);
  }

  private void LogToDb(string text)
  {
    // log to db.
  }
}

Key semantic parts in the decorator are:

  • Has a component of decorated behaviour (ILogger);
  • Component of decorator behaviour and decorator itself both implement the same interface (ILogger).

Example

// logs to console
var logger = new Logger();
// logs to console and db
var loggerWithDb = new LoggerWithDb(logger);
loggerWithDb.Log("Hello");

Managing multiple combinations of decorators

There are two major ways of how multiple decorators could be managed: extension methods and builder pattern.

Builder pattern

A builder could define how decorator is initialized on top of existing functionality. For example:

public class LoggerBuilder
{
  private ILogger _logger;

  // We start from any logger, could already be decorated.
  public LoggerBuilder(ILogger logger)
  {
    _logger = logger;
  }

  public LoggerBuilder()
  {
    // or plain console logger.
    _logger = new Logger();
  }

  public LoggerBuilder WithDb()
  {
    _logger = new LoggerWithDb(_logger);
   return this;
  }

  // Imagine there is one more behavior for logging
  public LoggerBuilder WithFile()
  {
    _logger = new LoggerWithFile(_logger);
    return this;
  }
  
  public void Build() => _logger; 
}

Example

var logger = new LoggerBuilder() .WithDb() .WithFile() .Build();

Extension methods

The same thing we did with a builder pattern can be done in a static extensions class. The end result is that we won't even need an extra class for decorations:

public static class LoggerDecorators
{
  public static ILogger WithDb(this ILogger logger) => new LoggerWithDb(logger);
  public static ILogger WithFile(this ILogger logger) => new LoggerWithFile(logger);
}

Example

var logger = new Logger()
                 .WithDb()
                 .WithFile();