CodeOath
← All posts
Architecture & Patterns80 min total · 26 parts

ACID, SOLID, and Common Design Patterns: A Software Design Reference

Contents — Part 17 of 26: Structural Patterns: Decorator
Part 17 of 26 · ~1 min

Structural Patterns: Decorator

Decorator attaches additional behavior to an object at runtime by wrapping it in another object that implements the same interface, instead of creating a new subclass for every possible combination of features — which would grow combinatorially (a LoggingCachingRetryingHttpClient class is not a scalable answer).

public interface IDataService
{
    string GetData(string key);
}

public class DataService : IDataService
{
    public string GetData(string key) => $"data for {key}"; // pretend this hits a real database
}

public class CachingDecorator : IDataService
{
    private readonly IDataService _inner;
    private readonly Dictionary<string, string> _cache = new();
    public CachingDecorator(IDataService inner) => _inner = inner;

    public string GetData(string key)
    {
        if (_cache.TryGetValue(key, out var cached)) return cached;
        var result = _inner.GetData(key);
        _cache[key] = result;
        return result;
    }
}

public class LoggingDecorator : IDataService
{
    private readonly IDataService _inner;
    public LoggingDecorator(IDataService inner) => _inner = inner;

    public string GetData(string key)
    {
        Console.WriteLine($"Fetching {key}");
        return _inner.GetData(key);
    }
}

// Decorators stack — each layer adds one concern, composed at runtime.
IDataService service = new LoggingDecorator(new CachingDecorator(new DataService()));
var result = service.GetData("user:1"); // logs, then checks cache, then hits the real service

Because every decorator implements the same IDataService interface as the object it wraps, calling code never has to know how many layers deep the decoration goes — it just calls GetData and every wrapped concern runs transparently. This is also exactly how ASP.NET Core's middleware pipeline works conceptually, and is the same idea behind Python decorators and Java's InputStream wrapper classes (BufferedInputStream wrapping a FileInputStream, and so on).