StackPractices
intermediate Por Mathias Paulenko

Patrón Chain of Responsibility

Pasa solicitudes a lo largo de una cadena de manejadores hasta que uno la procese. Un patrón de comportamiento para desacoplar emisores y receptores.

Temas: design

Visión General

El Patrón Chain of Responsibility es un patrón de diseño de comportamiento que te permite pasar solicitudes a lo largo de una cadena de manejadores. Cada manejador decide si procesa la solicitud o la pasa al siguiente manejador en la cadena. Esto desacopla emisores de receptores y permite que múltiples objetos manejen una solicitud sin que el emisor sepa cuál lo hará.

Cuándo Usarlo

Usa el Patrón Chain of Responsibility cuando:

  • Más de un objeto puede manejar una solicitud, y el manejador no se conoce de antemano
  • Quieres emitir una solicitud a uno de varios objetos sin especificar el receptor explícitamente
  • El conjunto de objetos que pueden manejar una solicitud debe especificarse dinámicamente
  • Necesitas un pipeline o middleware donde cada paso pueda procesar, transformar o detener una solicitud

Solución

Python

from abc import ABC, abstractmethod
from typing import Optional

class Handler(ABC):
    def __init__(self):
        self._next: Optional['Handler'] = None

    def set_next(self, handler: 'Handler') -> 'Handler':
        self._next = handler
        return handler  # Habilita encadenamiento fluido

    @abstractmethod
    def handle(self, request: str) -> Optional[str]:
        pass

    def _pass_to_next(self, request: str) -> Optional[str]:
        if self._next:
            return self._next.handle(request)
        return None

class AuthHandler(Handler):
    def handle(self, request: str) -> Optional[str]:
        if not request.startswith("token:"):
            return "401 No Autorizado"
        return self._pass_to_next(request)

class RateLimitHandler(Handler):
    def __init__(self):
        super().__init__()
        self.requests = 0
        self.limit = 3

    def handle(self, request: str) -> Optional[str]:
        self.requests += 1
        if self.requests > self.limit:
            return "429 Demasiadas Solicitudes"
        return self._pass_to_next(request)

class DataHandler(Handler):
    def handle(self, request: str) -> Optional[str]:
        return f"Procesado: {request}"

# Construir la cadena
handler = AuthHandler()
handler.set_next(RateLimitHandler()).set_next(DataHandler())

print(handler.handle("token:abc123"))  # Procesado
print(handler.handle("bad-request"))    # 401 No Autorizado

JavaScript

class Handler {
  constructor() {
    this.nextHandler = null;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  handle(request) {
    if (this.nextHandler) {
      return this.nextHandler.handle(request);
    }
    return null;
  }
}

class AuthHandler extends Handler {
  handle(request) {
    if (!request.startsWith("token:")) {
      return "401 No Autorizado";
    }
    return super.handle(request);
  }
}

class RateLimitHandler extends Handler {
  constructor() {
    super();
    this.requests = 0;
    this.limit = 3;
  }

  handle(request) {
    this.requests++;
    if (this.requests > this.limit) {
      return "429 Demasiadas Solicitudes";
    }
    return super.handle(request);
  }
}

class DataHandler extends Handler {
  handle(request) {
    return `Procesado: ${request}`;
  }
}

// Construir la cadena
const handler = new AuthHandler();
handler.setNext(new RateLimitHandler()).setNext(new DataHandler());

console.log(handler.handle("token:abc123")); // Procesado
console.log(handler.handle("bad-request"));     // 401

Java

public abstract class Handler {
    protected Handler next;

    public Handler setNext(Handler next) {
        this.next = next;
        return next;
    }

    public abstract String handle(String request);

    protected String passToNext(String request) {
        if (next != null) {
            return next.handle(request);
        }
        return null;
    }
}

public class AuthHandler extends Handler {
    @Override
    public String handle(String request) {
        if (!request.startsWith("token:")) {
            return "401 No Autorizado";
        }
        return passToNext(request);
    }
}

public class RateLimitHandler extends Handler {
    private int requests = 0;
    private final int limit = 3;

    @Override
    public String handle(String request) {
        requests++;
        if (requests > limit) {
            return "429 Demasiadas Solicitudes";
        }
        return passToNext(request);
    }
}

public class DataHandler extends Handler {
    @Override
    public String handle(String request) {
        return "Procesado: " + request;
    }
}

// Construir la cadena
Handler handler = new AuthHandler();
handler.setNext(new RateLimitHandler()).setNext(new DataHandler());

System.out.println(handler.handle("token:abc")); // Procesado
System.out.println(handler.handle("bad"));        // 401

Explicación

El Patrón Chain of Responsibility tiene dos roles:

  • Interfaz de Manejador — declara un método handle() y mantiene una referencia al siguiente manejador
  • Manejadores Concretos — implementan lógica de procesamiento; cada uno decide si maneja la solicitud o la pasa adelante

El cliente construye el orden de la cadena. Cada manejador también puede elegir detener la cadena (cortocircuito) retornando temprano sin llamar al siguiente manejador.

Variantes

VarianteEstructuraIdeal Para
Cadena LinealLista enlazada de manejadoresProcesamiento secuencial simple
Cadena en ÁrbolManejadores organizados jerárquicamenteÁrboles de decisión multinivel
Pipeline de MiddlewareArray de funciones, cada una llama next()Frameworks web (Express, Django middleware)
Bus de EventosManejadores se registran para eventos específicosSistemas desacoplados orientados a eventos

Lo que funciona

  • Mantén los manejadores enfocados — cada manejador debe hacer una sola cosa (auth, validación, logging, etc.)
  • Proporciona un manejador por defecto al final de la cadena para evitar solicitudes no manejadas
  • Permite modificación de cadena en tiempo de ejecución exponiendo métodos setNext() o addHandler()
  • Usa objetos de solicitud inmutables para que los manejadores no modifiquen estado compartido accidentalmente
  • Considera el orden cuidadosamente — los manejadores que cortocircuitan (auth, rate limiting) deben ir primero

Errores Comunes

  • Crear cadenas circulares donde un manejador eventualmente se llama a sí mismo, causando bucles infinitos
  • Olvidar llamar al siguiente manejador, descartando silenciosamente solicitudes que deberían haberse procesado
  • Colocar manejadores lentos o bloqueantes temprano en la cadena, causando latencia innecesaria para solicitudes rechazadas
  • Almacenar estado mutable en manejadores que se reutilizan entre solicitudes, causando contaminación cruzada
  • Construir cadenas excesivamente largas que se vuelven difíciles de depurar
  • No proporcionar un manejador por defecto al final de la cadena, dejando solicitudes sin manejar
  • Mezclar preocupaciones dentro de un solo manejador en lugar de mantenerlos enfocados
  • No documentar dependencias de manejadores y orden esperado
  • Fallar al manejar excepciones en manejadores, causando que toda la cadena falle
  • Usar el patrón de cadena cuando un condicional simple sería suficiente

Mejores Prácticas

  1. Mantén los manejadores de responsabilidad única. Cada manejador debe manejar una preocupación específica (auth, validación, logging, etc.) para mantener claridad y testabilidad.

  2. Proporciona un manejador por defecto. Siempre incluye un manejador comodín al final de la cadena para manejar solicitudes que caen a través, previniendo fallos silenciosos.

  3. Documenta el orden de manejadores. Documenta claramente el orden esperado de manejadores y cualquier dependencia entre ellos, ya que el orden afecta considerablemente el comportamiento.

  4. Usa objetos de solicitud inmutables. Pasa objetos de solicitud inmutables a través de la cadena para prevenir que los manejadores modifiquen accidentalmente estado compartido.

  5. Maneja excepciones gracefulmente. Cada manejador debería capturar y manejar sus propias excepciones, o envolverlas apropiadamente para prevenir fallo de cadena.

  6. Considera el impacto de rendimiento. Coloca manejadores rápidos de cortocircuito (auth, rate limiting) temprano en la cadena para fallar rápido y evitar procesamiento innecesario.

  7. Soporta reconfiguración de cadena. Permite que los manejadores se añadan, eliminen o reordenen en tiempo de ejecución para flexibilidad.

  8. Añade logging y monitoreo. Incluye manejadores de logging para trazar el flujo de solicitudes a través de la cadena e identificar cuellos de botella o fallos.

  9. Evita referencias circulares. Asegura que la estructura de cadena sea acíclica para prevenir bucles infinitos durante el procesamiento de solicitudes.

  10. Prueba manejadores en aislamiento. Escribe pruebas unitarias para cada manejador independientemente, luego pruebas de integración para la cadena completa.

Preguntas frecuentes

¿Es lo mismo que el middleware en frameworks web?

Sí. El middleware de Express.js, Django y ASP.NET Core son implementaciones del Patrón Chain of Responsibility. Cada middleware decide si procesa la solicitud, la pasa adelante, o cortocircuita con una respuesta.

¿Qué pasa si ningún manejador procesa la solicitud?

Por defecto, la solicitud cae a través de la cadena sin ser manejada. Considera agregar un Decorator para enriquecimiento o un manejador por defecto. Deberías agregar un manejador comodín al final que retorne una respuesta por defecto o lance un error apropiado.

¿Los manejadores pueden modificar la solicitud antes de pasarla?

Sí. A diferencia de una cadena pura de pasar-o-fallar, los manejadores pueden transformar, enriquecer o validar la solicitud antes de reenviarla. Esto es común en pipelines de middleware.

¿Cómo manejo operaciones asíncronas en una cadena?

Usa patrones async/await donde cada manejador retorna una promesa. La cadena espera la finalización de cada manejador antes de pasar al siguiente. Esto es esencial para operaciones I/O-bound como consultas de base de datos o llamadas API.

¿Debería usar este patrón para manejo de eventos?

Sí. Los sistemas de eventos a menudo usan una variación de este patrón donde múltiples event listeners (manejadores) pueden procesar un evento. A diferencia de una cadena lineal, los sistemas de eventos típicamente broadcastean a todos los manejadores registrados.

¿Cómo añado logging para trazar el flujo de solicitudes?

Añade un manejador de logging que registra la solicitud antes y después de cada manejador. Incluye timestamps y nombres de manejador para trazar el path de ejecución e identificar cuellos de botella de rendimiento.

¿Los manejadores pueden tener efectos secundarios?

Sí, los manejadores pueden realizar efectos secundarios como logging, colección de métricas, o actualizaciones de base de datos. Sin embargo, mantén los efectos secundarios enfocados y documentalos claramente para evitar comportamiento inesperado.

¿Cómo manejo escenarios de timeout en una cadena?

Implementa middleware de timeout que rastrea la duración de la solicitud y cortocircuita si el procesamiento excede un umbral. Esto previene que manejadores lentos bloqueen la cadena indefinidamente.

¿Deberían los manejadores ser sin estado?

Idealmente sí. Los manejadores sin estado son más fáciles de probar y reutilizar. Si el estado es necesario, asegúrate que esté scoped a la solicitud (almacenado en el objeto de contexto) en lugar de scoped al manejador para evitar contaminación cross-request.

¿Cómo implemento circuit breaking en una cadena?

Añade un manejador de circuit breaker que rastrea tasas de fallo y cortocircuita solicitudes cuando un servicio downstream está fallando. Esto previene fallos en cascada y mejora la resiliencia del sistema.