Abby's Digital Cafe

Patrón Estructural - Decorator

por Abigail Palmero (Abbytec)

¿Qué es el patrón estructural Decorator?

El patrón Decorator (o “Decorador”) es una forma de agregar funcionalidades a un objeto sin modificar su código original. En vez de crear muchas subclases (por ejemplo, “ServicioConLog”, “ServicioConCache”, “ServicioConLogYCache”…), envolvés el objeto con “capas” que aportan comportamiento extra. Cada capa conserva la misma interfaz del objeto original, así podés combinarlas como piezas de LEGO.

Pensalo como cuando comprás un café: el “café base” es el producto principal, y luego le sumás extras (leche, chocolate, crema) sin dejar de ser un café. Cada extra “envuelve” al café y agrega precio y descripción.

¿Cuándo conviene usar Decorator (y cuándo no)?

  • Cuando querés sumar o combinar funcionalidades de forma flexible (sin tocar la estructura de la clase original).
  • Cuando notás que la herencia se vuelve un árbol enorme de variantes.
  • Cuando querés activar/desactivar características por configuración (por ejemplo, en desarrollo tener logs y en producción no).
  • No es ideal si solo necesitás 1 variante simple y nunca va a crecer: quizá un parámetro o una clase directa alcance.

Idea clave: misma interfaz, capas opcionales

El truco del Decorator es que tanto el objeto original como los decoradores comparten una misma interfaz (por ejemplo, un método send() o getPrice()). El decorador guarda una referencia al objeto “envuelto” y, al ejecutar el método, puede: (1) hacer algo antes, (2) delegar al objeto envuelto y (3) hacer algo después.

Caso de uso 1 (TypeScript): envío de notificaciones con logging

Escenario realista: tenés un sistema que envía notificaciones por email. Luego te piden registrar (log) cada envío para auditarlo. En vez de modificar el envío base o crear otra clase gigante, agregás un decorador.

[typescript]
interface Notifier { send(to: string, message: string): void; } class EmailNotifier implements Notifier { send(to: string, message: string): void { // Simula el envío real console.log(`Enviando EMAIL a ${to}: ${message}`); } } // Decorador base (opcional pero común para reutilizar) abstract class NotifierDecorator implements Notifier { constructor(protected readonly wrappee: Notifier) {} send(to: string, message: string): void { this.wrappee.send(to, message); } } class LoggingNotifier extends NotifierDecorator { send(to: string, message: string): void { const start = Date.now(); console.log(`[LOG] Inicio envío a ${to}`); try { super.send(to, message); const ms = Date.now() - start; console.log(`[LOG] Envío OK a ${to} (${ms} ms) (estimación)`); } catch (err) { console.log(`[LOG] Envío FALLÓ a ${to}: ${String(err)}`); throw err; } } } // Uso const base: Notifier = new EmailNotifier(); const withLogging: Notifier = new LoggingNotifier(base); withLogging.send("[email protected]", "Tu código pasó las pruebas.");

Fijate en algo importante: no cambiamos EmailNotifier. El logging se agrega envolviendo el objeto. Si mañana te piden otro extra (por ejemplo, reintentos), lo agregás como otro decorador y podés combinarlos.

Paso a paso para construirlo

  1. Definí una interfaz simple (por ejemplo Notifier).
  2. Creá una implementación base (EmailNotifier).
  3. Creá un decorador que también implemente la interfaz y reciba otro Notifier por constructor.
  4. En el decorador, agregá comportamiento antes/después de delegar al objeto envuelto.
  5. En el uso final, envolvé el objeto base con 1 o más decoradores.

Caso de uso 2 (Java): precios de un pedido con extras

Escenario realista: estás armando un módulo de pedidos (por ejemplo, comida rápida). Tenés un “pedido base” y extras opcionales: envío a domicilio, regalo, seguro, etc. Decorator te deja sumar el costo y la descripción sin crear muchas subclases.

[java]
interface Order { String description(); double total(); } class BasicOrder implements Order { @Override public String description() { return "Pedido base"; } @Override public double total() { return 1200.0; // moneda local } } abstract class OrderDecorator implements Order { protected final Order wrappee; protected OrderDecorator(Order wrappee) { this.wrappee = wrappee; } @Override public String description() { return wrappee.description(); } @Override public double total() { return wrappee.total(); } } class DeliveryFee extends OrderDecorator { public DeliveryFee(Order wrappee) { super(wrappee); } @Override public String description() { return super.description() + " + Envío"; } @Override public double total() { return super.total() + 300.0; } } class GiftWrap extends OrderDecorator { public GiftWrap(Order wrappee) { super(wrappee); } @Override public String description() { return super.description() + " + Empaque de regalo"; } @Override public double total() { return super.total() + 150.0; } } // Uso public class Main { public static void main(String[] args) { Order order = new BasicOrder(); order = new DeliveryFee(order); order = new GiftWrap(order); System.out.println(order.description()); System.out.println("Total: " + order.total()); } }

Con esto podés activar extras según el caso: para algunos pedidos solo envío, para otros regalo + envío, etc. Y como todos son Order, el resto de tu sistema no se entera de la diferencia: solo llama total() y description().

Preguntas frecuentes (con respuestas claras)

¿Decorator es lo mismo que herencia?

No. Con herencia creás una clase nueva “fija” (y combinarlas puede explotar en cantidad). Con Decorator componés el comportamiento en tiempo de ejecución: hoy agregás logging, mañana no. Es más flexible cuando las combinaciones crecen.

¿Cuántos decoradores puedo encadenar?

Tantos como necesites, pero con sentido. Si terminás con 10 capas, puede volverse difícil de seguir. La recomendación es que cada decorador sea pequeño y que el armado de la cadena esté centralizado (por ejemplo, en una función “factory” o en la configuración).

¿Cómo sé que lo implementé bien?

Una señal es que podés reemplazar el objeto base por el decorado sin cambiar el resto del código: si todo depende de la misma interfaz, vas bien. Otra señal: tus decoradores no deberían conocer detalles internos del objeto envuelto; deberían tratarlo como “caja negra” y usar solo la interfaz.

Errores comunes al empezar con Decorator

  • Romper la interfaz: el decorador deja de comportarse como el original.
  • Hacer un decorador “Dios” que mete demasiadas responsabilidades.
  • Olvidar delegar: el decorador no llama al objeto envuelto y corta la funcionalidad base.
  • Encadenar decoradores sin una forma clara de construirlos (termina siendo confuso).

Resumen rápido para llevar

El patrón Decorator te permite agregar comportamiento sin modificar clases existentes, evitando una herencia llena de combinaciones. Usalo cuando necesites sumar extras opcionales (logs, reintentos, costos, validaciones) de forma flexible. Empezá por una interfaz simple, una implementación base y decoradores pequeños que envuelven y delegan.

Comentarios