-
Notifications
You must be signed in to change notification settings - Fork 541
Design Patterns: Decorator
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 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).
// logs to console
var logger = new Logger();
// logs to console and db
var loggerWithDb = new LoggerWithDb(logger);
loggerWithDb.Log("Hello");
There are two major ways of how multiple decorators could be managed: extension methods and 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;
}
var logger = new LoggerBuilder() .WithDb() .WithFile() .Build();
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);
}
var logger = new Logger()
.WithDb()
.WithFile();
Chapter 1: Fundamentals
Chapter 2: Object Oriented Programming (to be started 2/19/2020)
Chapter 3: Intermediate C# (Q2)
Chapter 4: Testing (Q2)
Chapter 5: SOLID (Q2)
Chapter 6: Desktop UI (Q2)
Chapter 7: Design Patterns (Q3)
Chapter 8: EF and enterprise patterns (Q3)
Chapter 9: Async programming (Q3)
Chapter 10: Web Api (Q3)
Chapter 11: Security (Q4)