Decorator
Añade responsabilidades a un objeto en runtime envolviéndolo en sucesivas capas.
Contexto
Quieres añadir logging, caching, retries o métricas a un componente sin modificarlo y sin crear una clase por combinación posible.
Problema
Las subclases producen una explosión: LoggingCachingRetryRepository. ¡Y se vuelve rígido!
Solución
Cada decorador implementa la misma interfaz que el objeto envuelto y delega, añadiendo su comportamiento antes/después.
#Ejemplo en 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);
}
}
// Composición:
IUserRepository repo = new LoggingUserRepository(
new CachingUserRepository(new SqlUserRepository(), cache), log);Cuándo NO aplicarlo
- Si el orden de los decoradores importa y no es obvio para el cliente.
- Si necesitas inspeccionar el objeto interno (rompes la transparencia).
Tradeoffs
| Pro | Contra |
|---|---|
| Composición flexible en runtime | Stack traces largos |
| Cumple SRP — cada capa hace una cosa | Difícil depurar el orden de envoltura |
#structural #gof #wrapper