Patrón Decorator
Añade nueva funcionalidad a objetos dinámicamente envolviéndolos. Patrón de diseño estructural para extensión flexible de comportamiento.
Visión general
El Patrón Decorator es un patrón de diseño estructural que te permite añadir nuevos comportamientos a objetos colocándolos dentro de objetos envolventes que contienen esos comportamientos. Proporciona una alternativa flexible a la herencia para extender funcionalidad.
Es ampliamente usado en streams de I/O (Java), pipelines de middleware (Express.js) y la sintaxis @decorator de Python.
Cuándo usarlo
Usa el Patrón Decorator cuando:
- Necesitas añadir responsabilidades a objetos dinámicamente y de forma transparente
- La extensión por herencia es impracticable o imposible (ej. clases final)
- Quieres combinar múltiples comportamientos en diversas configuraciones
- Necesitas adherirte al Principio de Responsabilidad Única separando preocupaciones
- Quieres evitar una explosión de clases de todas las combinaciones posibles por herencia
Solución
Python
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
class SimpleCoffee(Coffee):
def cost(self) -> float:
return 2.0
def description(self) -> str:
return "Simple coffee"
class MilkDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost() + 0.5
def description(self) -> str:
return self._coffee.description() + ", milk"
# Uso
coffee = MilkDecorator(SimpleCoffee())
print(coffee.description()) # Simple coffee, milk
print(coffee.cost()) # 2.5
JavaScript
class Coffee {
cost() {
return 2.0;
}
description() {
return "Simple coffee";
}
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 0.5;
}
description() {
return this.coffee.description() + ", milk";
}
}
// Uso
const coffee = new MilkDecorator(new Coffee());
console.log(coffee.description()); // Simple coffee, milk
console.log(coffee.cost()); // 2.5
Java
interface Coffee {
double cost();
String description();
}
class SimpleCoffee implements Coffee {
public double cost() { return 2.0; }
public String description() { return "Simple coffee"; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}
class MilkDecorator extends CoffeeDecorator {
MilkDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.5; }
public String description() { return coffee.description() + ", milk"; }
}
// Uso
Coffee coffee = new MilkDecorator(new SimpleCoffee());
System.out.println(coffee.description()); // Simple coffee, milk
System.out.println(coffee.cost()); // 2.5
Explicación
El Patrón Decorator se basa en composición sobre herencia:
- Interfaz Componente (
Coffee): Define el contrato tanto para componentes concretos como decorators - Componente concreto (
SimpleCoffee): El objeto base siendo envuelto - Decorator (
MilkDecorator): Implementa la misma interfaz y delega al objeto envuelto
Los decorators pueden anidarse arbitrariamente. Puedes envolver un MilkDecorator con un SugarDecorator, luego con un WhipDecorator, construyendo pilas de comportamiento en tiempo de ejecución.
Variantes
| Variante | Caso de uso | Compromiso |
|---|---|---|
| Basado en clases | Lenguajes fuertemente tipados (Java, C#) | Verboso pero type-safe |
| Basado en funciones | Sintaxis @decorator de Python | Conciso, pero composición menos explícita |
| Pipeline de middleware | Frameworks web (Express, Koa) | Excelente para procesamiento request/response |
Lo que funciona
- Mantén los decorators transparentes: Deben implementar exactamente la misma interfaz que el componente
- Delega todos los métodos: A menos que se sobrescriba intencionalmente, pasa cada llamada al objeto envuelto
- Evita decorators con estado cuando sea posible para reducir complejidad
- Documenta el orden de decorators: Algunos decorators pueden comportarse diferente dependiendo del orden de envoltura
- Prefiere composición sobre herencia: Esta es la filosofía central del patrón
Errores comunes
- Olvidar delegar: Un decorator que no reenvía llamadas rompe la cadena
- Abstracción filtrada: Decorators exponiendo métodos no presentes en la interfaz del componente
- Sensibilidad al orden: Decorators que dependen de ser internos o externos pueden causar bugs sutiles
- Sobre-decoración: Demasiados decorators anidados dificultan el debugging y profiling
- Conflictos de estado: Múltiples decorators con estado conflictivo sobre el mismo componente
Notas de Producción
- Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
- Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
- Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
- Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.
Puntos Clave
- Aplica patrón decorator cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
Temas Avanzados
Escenario: Decoradores para Logging y Cache
// Decorator pattern para añadir comportamiento sin modificar codigo
interface DataService {
getData(key: string): Promise<unknown>;
}
class APIDataService implements DataService {
async getData(key: string): Promise<unknown> {
const res = await fetch(`/api/data/${key}`);
return res.json();
}
}
// Decorador: Logging
class LoggingDecorator implements DataService {
constructor(private wrapped: DataService) {}
async getData(key: string): Promise<unknown> {
const start = Date.now();
console.log(`[LOG] getData(${key}) started`);
try {
const result = await this.wrapped.getData(key);
console.log(`[LOG] getData(${key}) OK in ${Date.now() - start}ms`);
return result;
} catch (err) {
console.error(`[LOG] getData(${key}) FAILED: ${err}`);
throw err;
}
}
}
// Decorador: Cache
class CacheDecorator implements DataService {
private cache = new Map<string, { value: unknown; expiry: number }>();
constructor(private wrapped: DataService, private ttlMs: number) {}
async getData(key: string): Promise<unknown> {
const cached = this.cache.get(key);
if (cached && cached.expiry > Date.now()) {
console.log(`[CACHE] HIT: ${key}`);
return cached.value;
}
console.log(`[CACHE] MISS: ${key}`);
const result = await this.wrapped.getData(key);
this.cache.set(key, { value: result, expiry: Date.now() + this.ttlMs });
return result;
}
}
// Composicion: API + Cache + Logging
const service = new LoggingDecorator(
new CacheDecorator(
new APIDataService(),
60000 // 60s TTL
)
);
// Resultado: cada llamada pasa por logging -> cache -> API
// Cache HIT: no llama a la API
// Cache MISS: llama a la API y guarda en cache
Lecciones:
- Decorador añade comportamiento sin modificar la clase original
- Los decoradores se componen: cache + logging + retry
- El orden importa: cache fuera de logging para no logear hits
- Mantiene Open/Closed: abierto a extension, cerrado a modificacion
- En TypeScript, usa class decorators (@decorator) para metadata
### Como ordeno multiples decoradores?
El orden importa. Pon el decorador mas barato fuera (cache) y el mas caro dentro (API). Logging fuera de cache: asi ves tanto hits como misses. Retry dentro de logging pero fuera de API: reintenta antes de fallar. Orden tipico: logging -> cache -> retry -> API. Cada decorador envuelve al siguiente, formando una cadena de responsabilidad.
End of document. Review and update quarterly.
## Troubleshooting
- **Pattern does not fit the problem**: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- **Too many abstractions**: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- **Tight coupling after refactoring**: check that interfaces are stable and dependencies point inward.
- **Tests break when the design changes**: favor stable contracts over internal structure.
- **Performance regression from indirection**: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
## Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón. Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Patrón Adapter
Convierte la interfaz de una clase en otra interfaz que los clientes esperan. Patrón de diseño estructural para compatibilidad de interfaces.
PatternPatrón Strategy
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Patrón de diseño conductual para selección flexible de comportamiento.
RecipeLlamar a una API REST: Python, JS, Java y Go
Cómo hacer peticiones HTTP a una API REST y manejar la respuesta JSON en Python, JavaScript, Java y Go.
PatternPatrón Bridge: Desacopla Abstracción e Implementación
Dividí una clase en dos jerarquías — abstracción e implementación — para que ambas evolucionen independientemente. Con ejemplos en Python, Java y JavaScript.
PatternPatrón Builder
Construye objetos complejos paso a paso. Patrón de diseño creacional para construcción de objetos legible y configurable.
PatternPatrón Chain of Responsibility
Pasa solicitudes a lo largo de una cadena de manejadores hasta que uno la procese. Un patrón de comportamiento para desacoplar emisores y receptores.