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 este patrón adecuado para proyectos pequeños?

Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.

¿Cómo se compara este patrón con alternativas?

Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.

¿Puedo aplicar este patrón parcialmente?

Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.