Ir al contenido

Lectura previa · para S8 L · lunes 29 de junio

Decorator

Agregar responsabilidades envolviendo, sin una clase por combinación · ~12 minutos de lectura

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.

La idea en una línea

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

The subclass explosion

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.

Señales rápidas
  • 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.
Autoevaluación

En la jerarquía de arriba, ¿cuál es el problema de diseño de fondo?

2 — Envolver en vez de heredar

Wrap, don’t inherit

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).

Señales rápidas
  • 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.
No confundir: Decorator no es lo mismo que un decorador de Python (@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.
Autoevaluación

¿Qué hace que un decorador pueda envolver a otro decorador y apilar capas?

3 — Apilar capas en runtime

Stacking layers at 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.

Señales rápidas
  • 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.
Autoevaluación

Después de aplicar Decorator, ¿cuál es la señal de que quedó bien?

4 — Decorator ya estaba en el framework

Decorator in the wild · when not to use

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.

Señales rápidas
  • 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.
Autoevaluación

¿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.