Patrón Decorator: Middleware y Comportamiento Dinámico

¿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--> ComponentCuá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:
- CacheDecorator: Verifica cache, si hay hit retorna
- AuthDecorator: Añade header Authorization
- LoggingDecorator: Registra inicio, tiempo
- BasicHttpClient: Ejecuta la petición real
- LoggingDecorator: Registra resultado
- 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).