Decorator

Adds responsibilities to an object at runtime by wrapping it in successive layers.

Context

You want to add logging, caching, retries, or metrics to a component without modifying it and without creating a class for every possible combination.

Problem

Subclasses lead to an explosion: LoggingCachingRetryRepository. And it becomes rigid!

Solution

Each decorator implements the same interface as the wrapped object and delegates, adding its behavior before/after.

#Example in C#

public interface IUserRepository { Task<User?> GetAsync(Guid id); }

public class SqlUserRepository : IUserRepository { /* DB */ public Task<User?> GetAsync(Guid id) => /* ... */ Task.FromResult<User?>(null); }

public class CachingUserRepository : IUserRepository
{
    private readonly IUserRepository _inner;
    private readonly IMemoryCache _cache;
    public CachingUserRepository(IUserRepository inner, IMemoryCache cache) { _inner = inner; _cache = cache; }

    public Task<User?> GetAsync(Guid id) =>
        _cache.GetOrCreateAsync(id, _ => _inner.GetAsync(id))!;
}

public class LoggingUserRepository : IUserRepository
{
    private readonly IUserRepository _inner;
    private readonly ILogger _log;
    public LoggingUserRepository(IUserRepository inner, ILogger log) { _inner = inner; _log = log; }
    public async Task<User?> GetAsync(Guid id)
    {
        _log.LogInformation("GetAsync {Id}", id);
        return await _inner.GetAsync(id);
    }
}

// Composition:
IUserRepository repo = new LoggingUserRepository(
    new CachingUserRepository(new SqlUserRepository(), cache), log);
When NOT to use it
  • If the order of decorators matters and is not obvious to the client.
  • If you need to inspect the inner object (you break transparency).
Tradeoffs
Pro Con
Flexible runtime composition Long stack traces
Adheres to SRP — each layer does one thing Difficult to debug wrapping order

#structural #gof #wrapper