cv.cm

Programación · Patrones de diseño

Patrón Decorator explicado con ejemplos

5 min de lectura

Respuesta corta

Decorator es un patrón de diseño estructural que añade responsabilidades a un objeto de forma dinámica envolviéndolo en otro objeto con la misma interfaz. Cada decorador reenvía las llamadas al objeto envuelto y añade su propio comportamiento antes o después, como registro, caché o compresión. Como los decoradores se pueden apilar en cualquier orden, son una alternativa flexible a crear una subclase para cada combinación de funciones.

Intención

Añadir responsabilidades adicionales a un objeto de forma dinámica. Los decoradores ofrecen una alternativa flexible a la herencia para extender la funcionalidad. Decorator es uno de los siete patrones estructurales del libro de la Banda de los Cuatro (Gang of Four, GoF), donde también se conoce como Wrapper.

El problema que resuelve

Imagina un almacén de datos con get(key). Algunos llamadores quieren registro, otros quieren una caché en memoria, otros ambas cosas, y otros quieren reintentos para un back end de red lento. Con herencia tendrías que escribir LoggingStore, CachingStore, LoggingCachingStore, RetryingCachingStore y así sucesivamente: una subclase por cada combinación, y las combinaciones quedan fijadas en tiempo de compilación.

Decorator convierte cada función en una pequeña clase envoltorio. Construyes la combinación que necesitas en tiempo de ejecución envolviendo un objeto dentro de otro, y el código que usa el almacén ve la misma interfaz sin importar cuántas capas haya.

Estructura y participantes

ParticipanteResponsabilidadEn el ejemplo
ComponenteLa interfaz compartida por los objetos reales y los decoradores.Store
Componente concretoEl objeto original que hace el trabajo principal.SlowStore
DecoradorMantiene una referencia a un componente e implementa la misma interfaz reenviando las llamadas.StoreDecorator
Decorador concretoAñade comportamiento antes o después de reenviar.LoggingStore, CachingStore

Implementación en TypeScript

interface Store {
  get(key: string): string;
}

class SlowStore implements Store {
  get(key: string): string { console.log(`  (database read: ${key})`); return key.toUpperCase(); }
}

// Base decorator: forwards everything to the wrapped store.
abstract class StoreDecorator implements Store {
  constructor(protected inner: Store) {}
  get(key: string): string { return this.inner.get(key); }
}

class LoggingStore extends StoreDecorator {
  get(key: string): string {
    console.log(`get ${key}`);
    return super.get(key);
  }
}

class CachingStore extends StoreDecorator {
  private cache = new Map<string, string>();
  get(key: string): string {
    if (!this.cache.has(key)) this.cache.set(key, super.get(key));
    return this.cache.get(key)!;
  }
}

// Stack features in any order at run time.
const store: Store = new LoggingStore(new CachingStore(new SlowStore()));
console.log(store.get("ada"));
console.log(store.get("ada"));
// get ada
//   (database read: ada)
// ADA
// get ada
// ADA

La segunda llamada se registra pero se responde desde la caché, así que la base de datos no se vuelve a leer. Si cambias el orden, new CachingStore(new LoggingStore(...)), solo se registran los fallos de caché. El orden de los decoradores cambia el comportamiento, lo que es a la vez una ventaja y una fuente habitual de confusión.

Decoradores de función en TypeScript

Cuando el componente es una sola función, un decorador puede ser una función de orden superior: toma una función y devuelve una nueva función con la misma firma y comportamiento adicional.

function withTiming<A extends unknown[], R>(name: string, fn: (...args: A) => R) {
  return (...args: A): R => {
    const start = performance.now();
    try {
      return fn(...args);
    } finally {
      console.log(`${name} took ${(performance.now() - start).toFixed(1)} ms`);
    }
  };
}

const slowSquare = (n: number) => { for (let i = 0; i < 1e6; i++); return n * n; };
const timedSquare = withTiming("square", slowSquare);
console.log(timedSquare(12)); // square took ... ms, then 144

Cuándo usarlo y cuándo no

Usa Decorator cuando:

  • Quieres añadir funciones como registro, caché, reintentos, validación, compresión o comprobaciones de acceso a objetos individuales, sin afectar a otros objetos de la misma clase.
  • Las funciones deben poder combinarse libremente, y la herencia produciría una clase por cada combinación.
  • No puedes o no debes cambiar la clase original, por ejemplo porque viene de una biblioteca.

Evítalo cuando:

  • Solo necesitas un comportamiento extra fijo. Añadirlo directamente, o usar una sola subclase, es más sencillo.
  • El código depende de la identidad del objeto o de su clase concreta. Un objeto decorado es un objeto distinto, así que comprobaciones como instanceof SlowStore o comparaciones con === fallan.
  • Muchas capas pequeñas dificultarían la depuración. Las pilas profundas de envoltorios producen pilas de llamadas largas y hacen poco claro qué capa hizo qué.

El middleware de los frameworks web, como Express o Koa, es una idea relacionada: cada capa recibe una petición, puede actuar sobre ella y la pasa a la siguiente.

Usos en el mundo real

  • Java I/O: new BufferedInputStream(new FileInputStream("data.bin")) envuelve un flujo en otro flujo que añade almacenamiento en búfer, y ambos tienen la interfaz InputStream. El propio libro GoF analiza los flujos como un ejemplo típico.
  • Colecciones de Java: Collections.unmodifiableList(list) y Collections.synchronizedList(list) devuelven envoltorios que añaden comportamiento de solo lectura o bloqueo alrededor de una lista existente.
  • Python: los decoradores de función como functools.lru_cache envuelven una función y añaden caché, y functools.wraps ayuda a que un envoltorio conserve el nombre y la docstring de la función envuelta.

Patrones relacionados

PatrónEn qué se diferencia de Decorator
AdapterCambia la interfaz de un objeto. Un decorador mantiene la interfaz y añade comportamiento.
ProxyTiene la misma estructura pero un propósito distinto: controla el acceso (creación diferida, acceso remoto, permisos) y normalmente gestiona por sí mismo el ciclo de vida del objeto real.
CompositeTambién usa composición recursiva, pero un compuesto contiene muchos hijos; un decorador envuelve exactamente uno.
StrategyCambia el interior de un objeto intercambiando un algoritmo. Un decorador cambia el exterior envolviéndolo.
Chain of ResponsibilityTambién pasa las peticiones por una serie de objetos, pero cada manejador puede detener la petición; los decoradores normalmente siempre reenvían.

Preguntas frecuentes

¿Cuál es la diferencia entre Decorator y la herencia?

La herencia añade comportamiento a toda una clase en tiempo de compilación. Un decorador añade comportamiento a un objeto en tiempo de ejecución, y varios decoradores pueden combinarse en cualquier orden.

¿Cuál es la diferencia entre Decorator y Proxy?

Tienen la misma forma. Un decorador añade funciones que el cliente elige apilar, mientras que un proxy controla el acceso al objeto real, a menudo creándolo o decidiendo si llamarlo siquiera.

¿Los @decorators de TypeScript son el patrón Decorator?

No exactamente. Los decoradores @ son una característica del lenguaje que modifica las clases y sus miembros cuando se definen. Pueden usarse para implementar el envoltorio, pero el patrón GoF trata de envolver objetos en tiempo de ejecución.

¿Importa el orden de los decoradores?

Sí. Cada decorador ve las llamadas antes que los objetos que envuelve, así que una caché colocada fuera de un registrador oculta al registrador las llamadas repetidas, mientras que un registrador fuera de la caché ve todas las llamadas.

Todos los tutoriales de programación