Saltar al contenidoJMRG
Todas las entradas

Patrón Decorator: Middleware y Comportamiento Dinámico

design-patternsstructuraldecorator

¿Qué es el Patrón Decorator?

El patrón Decorator adjunta responsabilidades adicionales a un objeto dinámicamente. Proporciona una alternativa flexible a la herencia para extender funcionalidad, permitiendo combinar comportamientos de forma modular sin modificar el código original.

classDiagram
    class Component {
        <<interface>>
        +operation()
    }

    class ConcreteComponent {
        +operation()
    }

    class Decorator {
        <<abstract>>
        -component: Component
        +operation()
    }

    class ConcreteDecoratorA {
        +operation()
        +addedBehavior()
    }

    class ConcreteDecoratorB {
        +operation()
        +addedState
    }

    Component <|.. ConcreteComponent
    Component <|.. Decorator
    Decorator <|-- ConcreteDecoratorA
    Decorator <|-- ConcreteDecoratorB
    Decorator o--> Component

Cuándo Usar Decorator

  • Añadir comportamiento a objetos sin modificar su código
  • Cuando la extensión mediante herencia no es práctica
  • Middleware, logging, caching, validación
  • Combinación dinámica de comportamientos

Implementación Paso a Paso

1. Definir la Interfaz del Componente

La interfaz HttpClient define el contrato que implementarán tanto el componente base como los decoradores:

interface HttpRequest {
  url: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  body?: unknown;
}

interface HttpResponse<T> {
  data: T;
  status: number;
  headers: Record<string, string>;
  duration?: number;
}

interface HttpClient {
  request<T>(req: HttpRequest): Promise<HttpResponse<T>>;
}

2. Implementar el Componente Concreto

BasicHttpClient es la implementación base sin funcionalidades adicionales:

class BasicHttpClient implements HttpClient {
  async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
    const response = await fetch(req.url, {
      method: req.method,
      headers: req.headers,
      body: req.body ? JSON.stringify(req.body) : undefined,
    });

    return {
      data: await response.json(),
      status: response.status,
      headers: Object.fromEntries(response.headers),
    };
  }
}

3. Crear el Decorador Base Abstracto

Todos los decoradores concretos heredan de esta clase:

abstract class HttpClientDecorator implements HttpClient {
  constructor(protected wrapped: HttpClient) {}

  async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
    return this.wrapped.request<T>(req);
  }
}

4. Implementar Decoradores Concretos

Cada decorador añade una funcionalidad específica:

/** Decorador que registra peticiones y tiempos de respuesta */
class LoggingDecorator extends HttpClientDecorator {
  async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
    const startTime = Date.now();
    console.log(`[HTTP] -> ${req.method} ${req.url}`);

    const response = await this.wrapped.request<T>(req);
    const duration = Date.now() - startTime;

    console.log(`[HTTP] <- ${response.status} (${duration}ms)`);
    return { ...response, duration };
  }
}

/** Decorador que añade autenticación */
class AuthDecorator extends HttpClientDecorator {
  constructor(
    wrapped: HttpClient,
    private getToken: () => string
  ) {
    super(wrapped);
  }

  async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
    const token = this.getToken();
    const headers = {
      ...req.headers,
      Authorization: `Bearer ${token}`,
    };
    return this.wrapped.request<T>({ ...req, headers });
  }
}

/** Decorador que implementa cache para peticiones GET */
class CacheDecorator extends HttpClientDecorator {
  private cache = new Map<string, { data: unknown; expiry: number }>();

  constructor(
    wrapped: HttpClient,
    private ttl: number
  ) {
    super(wrapped);
  }

  async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
    if (req.method !== 'GET') {
      return this.wrapped.request<T>(req);
    }

    const cached = this.cache.get(req.url);
    if (cached && cached.expiry > Date.now()) {
      console.log(`[CACHE] HIT: ${req.url}`);
      return cached.data as HttpResponse<T>;
    }

    const response = await this.wrapped.request<T>(req);
    this.cache.set(req.url, {
      data: response,
      expiry: Date.now() + this.ttl,
    });

    return response;
  }
}

5. Componer Decoradores

Los decoradores se apilan en orden (el orden importa):

// Cliente base
let client: HttpClient = new BasicHttpClient();

// Aplicar decoradores de adentro hacia afuera
client = new LoggingDecorator(client);
client = new AuthDecorator(client, () => 'my-token');
client = new CacheDecorator(client, 60000);

// Usar como cliente normal
const response = await client.request<User[]>({
  method: 'GET',
  url: '/api/users',
});

Flujo de Ejecución

Cuando se llama request:

  1. CacheDecorator: Verifica cache, si hay hit retorna
  2. AuthDecorator: Añade header Authorization
  3. LoggingDecorator: Registra inicio, tiempo
  4. BasicHttpClient: Ejecuta la petición real
  5. LoggingDecorator: Registra resultado
  6. CacheDecorator: Almacena en cache

Decoradores Disponibles

Decorador Funcionalidad
LoggingDecorator Registra peticiones y tiempos
AuthDecorator Añade token de autenticación
CacheDecorator Cache de respuestas GET con TTL
RetryDecorator Reintenta con backoff exponencial
TimeoutDecorator Cancela peticiones lentas

Resumen

Aspecto Descripción
Propósito Añadir comportamiento dinámicamente sin modificar clases
Componentes Component, ConcreteComponent, Decorator, ConcreteDecorators
Ventaja Composición flexible, principio Open/Closed
Desventaja Muchos objetos pequeños, orden de decoradores importa

Conclusión

El patrón Decorator es ideal para middleware y cross-cutting concerns. Permite añadir funcionalidades como logging, caching, y autenticación de forma modular y reutilizable.

En el próximo artículo exploraremos el Patrón Facade, que simplifica el acceso a sistemas complejos.


Basado en "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).