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