Decorator Pattern
Intención y problema
¿Cómo agregar responsabilidades de manera componible sin modificar la implementación base ni crear una subclase por combinación? Decorator envuelve un Component mediante un contrato compatible y agrega comportamiento alrededor de su colaboración.
Pertenece a structural-patterns. La intención clásica GoF de extender responsabilidades se recoge en el material académico de CMU. El escenario de notificaciones y los contrastes de orden siguientes son ejemplos propios.
Supongamos que EmailSender envía una notificación. Algunos consumidores necesitan logging; otros, métricas; otros, una política de repetición bajo condiciones seguras. Incorporar todo en EmailSender mezcla envío con responsabilidades opcionales. Una subclase para cada combinación puede crecer sin representar variantes cohesivas del envío.
Forces y contrato común
Queremos seleccionar responsabilidades por composición y conservar una conversación utilizable para el cliente. También necesitamos entender costo, orden, errores y ownership. Que todos los wrappers tengan la misma firma no demuestra que preserven las obligaciones observables.
El contrato NotificationSender debería expresar qué significa enviar, qué resultado devuelve y cómo comunica fallos. Si se promete una operación síncrona, un wrapper que solo la encola modifica una obligación relevante. Si se permite logging adicional, hay que decidir si un fallo del logger impide enviar o se registra por otra vía.
Otra tensión es entre configuración flexible y comprensión. Poder combinar capas no implica que toda combinación sea válida o conveniente. El ensamblaje debe hacer visibles las decisiones que afectan resultados, latencia y recursos.
Estructura y colaboración
Client → NotificationSender
↑ compatible implementations
├── EmailSender
└── Decorator → NotificationSender
MetricsDecorator
→ LoggingDecorator
→ EmailSenderComponent es el contrato común. Concrete Component realiza la capacidad base. Un Decorator conserva una referencia a otro Component y expone una conversación compatible. Los decorators concretos agregan responsabilidades antes, después o alrededor de la delegación.
La composición conceptual:
Metrics(
Logging(
EmailSender
)
)recibe una llamada desde afuera hacia adentro. Los resultados y errores regresan por esas capas; cada una puede medir, registrar o aplicar una política según su contrato. No es obligatorio delegar exactamente una vez: un wrapper puede repetir o impedir una operación, pero esa elección necesita semántica explícita y compatibilidad con lo prometido.
Fowler muestra cómo decorators pueden agregar responsabilidades a un Command, incluyendo coordinación transaccional. Decorated Command. Su ejemplo ilustra combinación de intenciones; no justifica envolver toda operación del sistema.
Decorator frente a herencia
Una familia basada en combinaciones puede terminar así:
Base
├── LoggingBase
├── RetryBase
├── LoggingRetryBase
└── CachedLoggingRetryBaseComponer responsabilidades puede evitar esa multiplicación de subclasses y permitir variantes por instancia. Cambiar medición sin reescribir envío también puede aislar responsabilidades con razones de cambio distintas.
A cambio aparecen wrappers, ensamblaje y más pasos de ejecución. La herencia puede ser adecuada cuando expresa una especialización coherente y estable o cuando un framework establece ese contrato de extensión. Composition no es siempre mejor que inheritance. Hay que comparar qué cambio queda protegido y qué costo agrega cada organización.
Un conjunto pequeño de variantes fijas puede ser más claro mediante implementaciones directas. Si todos los consumers requieren siempre la misma responsabilidad y no hay composición significativa, agregar capas puede complicar una capacidad que debería explicarse como una unidad.
El orden introduce semántica
Para el ejemplo, supongamos que Metrics mide una invocación a su componente y que Retry repite únicamente los fallos clasificados como repetibles:
Retry(Metrics(Operation))
Metrics(Retry(Operation))La primera composición mide cada intento. La segunda mide una operación lógica que puede contener varios intentos. La duración externa puede incluir esperas entre repeticiones; las mediciones internas pueden excluirlas. Ambas pueden ser correctas, pero responden preguntas diferentes.
| Composición bajo esa política | Qué observa Metrics |
|---|---|
| Retry(Metrics(Operation)) | Cada intento, sus resultados y su duración. |
| Metrics(Retry(Operation)) | El resultado final y tiempo total de la llamada lógica. |
El orden también puede cambiar cuántas veces se escribe un log, qué excepción observa una capa o si una cache almacena un fallo antes de que otra capa intente recuperarlo. No existe un orden universal: hay que nombrar las unidades y obligaciones de cada wrapper.
Las pruebas de composición deben incluir fallos intermedios y agotamiento de la política, además del caso exitoso. Verificar wrappers por separado no demuestra que su combinación conserve resultados, conteos y tiempos.
Retry y Timeout conservan sus conceptos
Una implementación de retry o timeout puede envolver una operación mediante composición semejante a Decorator. Eso no convierte esos conceptos de resiliencia en Decorators: sus canonicals describen políticas y consecuencias distintas de la organización del wrapper.
RetryDecorator alrededor de EmailSender no vuelve seguro repetir correos. Si un envío ocurrió y su confirmación se perdió, otra ejecución puede duplicar el efecto. idempotency ayuda a analizar esa obligación; la capacidad de delegar varias veces no constituye evidencia de que hacerlo sea correcto.
Tampoco un wrapper de timeout garantiza detener el trabajo interno o cancelar un efecto remoto. La compatibilidad debe incluir lo que el cliente puede afirmar después de un resultado incierto. La composición conserva o modifica obligaciones; no fabrica garantías por su nombre.
Decorator, Proxy y Adapter
Decorator agrega responsabilidades combinables. Proxy representa o controla acceso a otro objeto: adquisición lazy, autorización, representación remota o caching pueden motivarlo. Ambos pueden dibujarse Client → Wrapper → Real Component.
Una medición adicional suele explicar la intención Decorator; un sustituto que retrasa cargar un recurso suele explicar Proxy. En implementaciones reales las fronteras pueden difuminarse: un wrapper puede controlar acceso y registrar métricas. La identidad depende de intent, context, forces y consequences, no solamente del UML.
adapter-pattern busca compatibilidad con una interfaz esperada. Decorator generalmente conserva el contrato del componente mientras agrega comportamiento. La forma de delegación puede parecer idéntica; reconstruir el propósito evita clasificar todos los wrappers con un mismo nombre.
Middleware, pipeline y lenguaje
pipe-and-filter organiza procesamiento mediante etapas independientes y conexiones de datos. Decorator envuelve una capacidad detrás de un contrato compatible. Una cadena visualmente similar puede describir ejecución alrededor de una llamada o transformación entre etapas; esa diferencia importa para ownership y control.
Middleware puede combinar ideas de Decorator, Chain of Responsibility y procesamiento en pipeline. Que exista una llamada next no permite clasificar universalmente un framework. Hay que observar si todas las etapas transforman datos, si un receptor puede detener la cadena o si una responsabilidad envuelve al componente.
Higher-order functions y wrappers funcionales pueden conservar la intención con menos clases. Una función que recibe una operación y devuelve otra compatible puede agregar medición. Reducir ceremony no elimina reglas de orden, capturas de estado, errores ni lifecycle.
Consecuencias, límites y misuses
La composición facilita responsabilidades opcionales y reutilizables sin modificar la capacidad base. Puede hacer más simples las pruebas de cada obligación, siempre que también se verifique la combinación relevante.
Sus costos incluyen stack complexity, debugging a través de capas y configuración. La identidad del wrapper puede ser distinta de la del componente: igualdad, claves de cache o comparación de referencias requieren una política. También hay que decidir quién libera recursos para evitar cierres dobles o referencias retenidas.
Son misuses construir stacks profundos sin necesidad, introducir comportamiento costoso oculto, permitir combinaciones contradictorias y violar el contrato envuelto. Un logger que transforma un fallo de negocio en éxito no conserva transparencia porque compile. Una configuration explosion puede reproducir el problema que se quería evitar en la herencia.
pattern-selection orienta la decisión frente a una implementación directa; design-patterns conecta las familias. Usar Decorator se justifica cuando la composición protege responsabilidades reales y su semántica resulta más comprensible que las alternativas.