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