Saltar al contenidoJMRG
Todas las entradas

Patrón Chain of Responsibility: Pipelines de Procesamiento

design-patternsbehavioralchain-of-responsibility

Intención

Chain of Responsibility evita acoplar el emisor de una solicitud a su receptor dando a más de un objeto la oportunidad de manejar la solicitud. Encadena los objetos receptores y pasa la solicitud a lo largo de la cadena.

El Problema

Sin este patrón, el código de validación se vuelve un if-else interminable:

function handleRequest(req: Request) {
  if (!req.headers.auth) {
    return { status: 401, body: 'Unauthorized' };
  }
  if (rateLimiter.isExceeded(req.ip)) {
    return { status: 429, body: 'Too many requests' };
  }
  if (!validateBody(req.body)) {
    return { status: 400, body: 'Invalid body' };
  }
  // Finalmente, la lógica de negocio...
}

La Solución: Chain of Responsibility

classDiagram
    class Handler {
        <<interface>>
        +setNext(handler: Handler): Handler
        +handle(request): Response
    }

    class BaseHandler {
        -next: Handler
        +setNext(handler: Handler): Handler
        +handle(request): Response
    }

    class AuthHandler {
        +handle(request): Response
    }

    class RateLimitHandler {
        +handle(request): Response
    }

    Handler <|.. BaseHandler
    BaseHandler <|-- AuthHandler
    BaseHandler <|-- RateLimitHandler
    BaseHandler o--> Handler

Implementación TypeScript

interface HttpContext {
  request: {
    method: string;
    path: string;
    headers: Record<string, string>;
    body?: unknown;
    user?: { id: string; roles: string[] };
  };
  response: {
    status: number;
    body?: unknown;
  };
}

abstract class MiddlewareHandler {
  private next: MiddlewareHandler | null = null;

  setNext(handler: MiddlewareHandler): MiddlewareHandler {
    this.next = handler;
    return handler;
  }

  async handle(ctx: HttpContext): Promise<void> {
    const shouldContinue = await this.process(ctx);
    if (shouldContinue && this.next) {
      await this.next.handle(ctx);
    }
  }

  protected abstract process(ctx: HttpContext): Promise<boolean>;
}

// Handlers concretos
class AuthenticationMiddleware extends MiddlewareHandler {
  protected async process(ctx: HttpContext): Promise<boolean> {
    const token = ctx.request.headers['authorization'];

    if (!token?.startsWith('Bearer ')) {
      ctx.response.status = 401;
      ctx.response.body = { error: 'Authentication required' };
      return false;
    }

    ctx.request.user = { id: 'user_123', roles: ['user'] };
    return true;
  }
}

class RateLimitMiddleware extends MiddlewareHandler {
  private requests = new Map<string, number[]>();

  constructor(
    private limit: number,
    private windowMs: number
  ) {
    super();
  }

  protected async process(ctx: HttpContext): Promise<boolean> {
    const ip = ctx.request.headers['x-forwarded-for'] || 'unknown';
    const now = Date.now();
    const windowStart = now - this.windowMs;

    const timestamps = (this.requests.get(ip) || []).filter(t => t > windowStart);

    if (timestamps.length >= this.limit) {
      ctx.response.status = 429;
      ctx.response.body = { error: 'Too many requests' };
      return false;
    }

    timestamps.push(now);
    this.requests.set(ip, timestamps);
    return true;
  }
}

class LoggingMiddleware extends MiddlewareHandler {
  protected async process(ctx: HttpContext): Promise<boolean> {
    console.log(`[${new Date().toISOString()}] ${ctx.request.method} ${ctx.request.path}`);
    return true;
  }
}

// Pipeline builder
class MiddlewarePipeline {
  private first: MiddlewareHandler | null = null;
  private last: MiddlewareHandler | null = null;

  use(handler: MiddlewareHandler): this {
    if (!this.first) {
      this.first = handler;
      this.last = handler;
    } else {
      this.last!.setNext(handler);
      this.last = handler;
    }
    return this;
  }

  async execute(ctx: HttpContext): Promise<void> {
    if (this.first) {
      await this.first.handle(ctx);
    }
  }
}

// Uso
const pipeline = new MiddlewarePipeline()
  .use(new LoggingMiddleware())
  .use(new RateLimitMiddleware(100, 60000))
  .use(new AuthenticationMiddleware());

await pipeline.execute(ctx);

Cuándo Usar

Situación Recomendación
Middlewares HTTP ✅ Recomendado
Validación en capas ✅ Recomendado
Procesamiento de eventos ✅ Recomendado
Flujo simple lineal ❌ Considerar alternativas

Conclusión

Chain of Responsibility permite construir pipelines flexibles donde cada handler tiene una responsabilidad clara.