Agregar sin multiplicar clases
Esta es la lectura previa para la sesión presencial del lunes (S8 L). Abre el Bloque E — composición flexible: después de aislar dependencias (Adapter, Facade), ahora componemos comportamiento. Decorator responde una pregunta concreta: ¿cómo le agrego responsabilidades a un objeto —de a una, combinables— sin crear una clase por cada combinación posible?
Es un patrón estructural, y de los más presentes en el código que ya usás: los middlewares de Laravel y Express, los streams de Java, las capas de una respuesta HTTP. Cuando termines esta lectura vas a reconocerlo en todos ellos. Leela antes del lunes: arrancamos la clase dándola por vista.
Un decorador envuelve un objeto, comparte su interfaz y le suma una responsabilidad.
Como comparte la interfaz, puede envolver a otro decorador — y así apilás capas en runtime.
Tip de lectura: tocá, enfocá o pasá el mouse sobre las líneas marcadas para ver por qué importan.
Conceptos rápidos
En las anotaciones aparecen algunos términos. Si no los tenés frescos, esta es la versión corta:
- Componente: la interfaz común que comparten el objeto base y todos los decoradores. Es lo que los hace intercambiables.
- Componente concreto: el objeto base sin extras (
BasicShipping). Lo que va más adentro de la cebolla. - Decorador: implementa la misma interfaz, guarda una referencia al objeto interno y delega en él sumando su parte.
- Envolver / apilar: anidar decoradores uno dentro de otro para combinar responsabilidades en runtime.
- Delegar: llamar al método del objeto interno (
parent::cost(),super) y agregar lo propio al volver. - OCP (abierto/cerrado): el principio SOLID de S2 — abierto a extensión, cerrado a modificación. Decorator es OCP aplicado a responsabilidades apilables.
No tenés que memorizarlos. Si una anotación menciona uno y no lo recordás, volvé acá un momento y seguís.
1 — Una clase por cada combinación
Empecemos por el dolor concreto. Un envío de delivery puede llevar extras: seguro, empaque de regalo, entrega prioritaria. Cada extra suma un costo y modifica la descripción. La primera idea —la que casi todos tenemos— es hacer una subclase por cada caso: ShippingWithInsurance, ShippingWithGift, ShippingPriority.
El problema aparece cuando los extras se combinan. ¿Envío con seguro Y empaque de regalo? Otra clase. ¿Con los tres? Otra más. Con 3 extras opcionales son 8 combinaciones; con 4, dieciséis. La jerarquía explota — y cada extra nuevo duplica el número de clases.
// La idea ingenua: una subclase por combinación class Shipping { public function cost(): float { return 5.00; } } class ShippingWithInsurance extends Shipping { ... } class ShippingWithGift extends Shipping { ... } class ShippingPriority extends Shipping { ... } class ShippingWithInsuranceAndGift extends Shipping { ... } class ShippingWithInsuranceAndPriority extends Shipping { ... } class ShippingWithGiftAndPriority extends Shipping { ... } class ShippingWithAllThree extends Shipping { ... }
El problema de fondo es que estás usando herencia para algo que es composición. Los extras no son tipos distintos de envío: son capas que se apilan sobre un envío. La herencia te obliga a fijar la combinación en tiempo de compilación, una clase por cada permutación posible.
La pregunta de diseño: ¿cómo le agrego responsabilidades a un objeto —de a una, combinables en cualquier orden— sin crear una clase por combinación? Esa es exactamente la pregunta que responde Decorator.
- Tenés una clase base y necesitás variantes que se combinan entre sí.
- El número de subclases crece de forma multiplicativa, no lineal, con cada feature nueva.
- Las subclases repiten lógica porque comparten extras que no pueden compartir código.
En la jerarquía de arriba, ¿cuál es el problema de diseño de fondo?
2 — Envolver en vez de heredar
Pensá en cómo te vestís. Tenés un cuerpo (el objeto base) y le ponés una camisa, encima un suéter, encima un abrigo. Cada prenda envuelve a lo de abajo y le suma algo —abrigo, estilo— sin cambiar el cuerpo. Te las ponés y las quitás en runtime, en el orden que quieras. Nadie crea una clase CuerpoConCamisaYSuéterYAbrigo.
Esa es la idea de Decorator: en vez de heredar, envolvés. Un decorador implementa la misma interfaz que el objeto que envuelve, guarda una referencia a él, y en cada método delega al objeto interno y le agrega su parte. Como comparte la interfaz, un decorador puede envolver a otro decorador — y así apilás capas.
// La estructura de Decorator (conceptual) interface Shipping { public function cost(): float; } class BasicShipping implements Shipping { public function cost(): float { return 5.00; } } abstract class ShippingDecorator implements Shipping { public function __construct(protected Shipping $inner) {} public function cost(): float { return $this->inner->cost(); } }
La definición canónica (GoF): "Decorator agrega responsabilidades a un objeto dinámicamente. Provee una alternativa flexible a la herencia para extender funcionalidad." Las dos palabras clave: dinámicamente (en runtime, no en compilación) y alternativa a la herencia (componés en vez de extender).
- El decorador implementa la misma interfaz que el objeto que envuelve.
- Guarda una referencia al objeto interno y delega en él, sumando su parte.
- Como comparten interfaz, un decorador puede envolver a otro: las capas se apilan.
@staticmethod) ni que los decorators de TypeScript/Angular. Comparten el nombre y la metáfora de "envolver", pero el patrón GoF es sobre objetos que envuelven objetos compartiendo una interfaz, no sobre azúcar sintáctico del lenguaje.¿Qué hace que un decorador pueda envolver a otro decorador y apilar capas?
3 — Apilar capas en runtime
Acá está el patrón completo. Cada extra es un decorador concreto que extiende ShippingDecorator, delega al objeto interno con parent::cost() (o super) y le suma su costo. El "aha" llega al final: armás la combinación que quieras envolviendo, en runtime, sin ninguna clase combinada.
Fijate el orden de la última parte: BasicShipping es el centro, y cada decorador lo va envolviendo como capas de cebolla. Cuando llamás cost() en el de afuera, la llamada baja hasta el centro y cada capa suma su parte al volver.
// Decoradores concretos — uno por extra class InsuranceDecorator extends ShippingDecorator { public function cost(): float { return parent::cost() + 2.50; } } class GiftWrapDecorator extends ShippingDecorator { public function cost(): float { return parent::cost() + 1.00; } } class PriorityDecorator extends ShippingDecorator { public function cost(): float { return parent::cost() + 4.00; } } // El "aha": armás la combinación envolviendo, en runtime $ship = new BasicShipping(); // 5.00 $ship = new InsuranceDecorator($ship); // + 2.50 $ship = new GiftWrapDecorator($ship); // + 1.00 $ship->cost(); // 8.50 — base + seguro + regalo
El termómetro de que Decorator quedó bien aplicado: agregar un extra nuevo es un decorador nuevo, sin tocar el base ni los otros decoradores. Y armar cualquier combinación es solo envolver en distinto orden. Pasaste de 2ⁿ clases a n decoradores — de crecimiento exponencial a lineal.
El costo: muchas capas pueden ser difíciles de depurar (una pila de wrappers), y el orden a veces importa. Pero a cambio ganás combinaciones ilimitadas sin explosión de clases.
- Cada decorador concreto agrega una responsabilidad y delega el resto al interno.
- Las combinaciones se arman en runtime envolviendo, no con clases predefinidas.
- Agregar una capa nueva no obliga a tocar el componente base ni las otras capas.
Después de aplicar Decorator, ¿cuál es la señal de que quedó bien?
4 — Decorator ya estaba en el framework
Como con Observer, Decorator ya lo venís usando sin nombrarlo. El caso más claro: los middlewares. Una petición HTTP entra y atraviesa capas —autenticación, logging, CORS, rate-limit— cada una envolviendo a la siguiente y agregando su parte antes de pasar al núcleo. Eso es Decorator: cada middleware envuelve al handler y delega hacia adentro.
Lo mismo los streams de Java (BufferedReader envuelve un FileReader y le suma buffering) o las respuestas HTTP que se van envolviendo con compresión, cache, etc. Ahora sabés qué patrón hay debajo — y por qué podés apilar comportamientos sin tocar el núcleo.
// Laravel: el middleware pipeline es Decorator class Authenticate { public function handle($request, Closure $next) { // ... su parte (autenticar) ... return $next($request); // delega hacia adentro } } // ⚠ Cuándo NO usar Decorator $ship = new BasicShipping(); // un único caso → no envuelvas por envolver
El costo de Decorator es real: una pila profunda de wrappers es difícil de depurar (¿en qué capa se sumó esto?), y el orden de envoltura a veces cambia el resultado. Por eso no lo apliques cuando hay un solo comportamiento fijo que nunca se combina —ahí una subclase o una llamada directa es más honesta— ni cuando necesitás que las capas se conozcan entre sí.
Esto abre el Bloque E — composición flexible. El lunes arrancamos Decorator presencial dando esta lectura por vista, y la semana sigue con Composite (árboles de objetos) y Chain of Responsibility (cadenas de manejadores). Los tres comparten una idea: componer objetos en estructuras flexibles en vez de cablear comportamiento por herencia.
- Aparece un pipeline o cadena de capas (middlewares, streams): casi siempre hay Decorator debajo.
- Podés sumar una capa de comportamiento envolviendo, sin tocar el núcleo.
- Si solo hay un comportamiento fijo y único, probablemente NO necesitás Decorator.
¿Cuándo es un error aplicar Decorator?
Para ir más lejos
La fuente y material para seguir:
- Refactoring.guru. Decorator — el patrón con diagramas y ejemplos paso a paso.
- Gamma, Helm, Johnson, Vlissides (1994). Design Patterns. El capítulo de Decorator, para la formulación original GoF.
- Laravel. Middleware — Decorator provisto por el framework, el pipeline de capas sobre la request.