El patrón Chain of Responsibility desacopla el emisor de una solicitud de su receptor, permitiendo que varios objetos tengan la oportunidad de manejar la petición. Los handlers se encadenan para delegar el procesamiento hasta que uno decida atenderla o la cadena se agote.
Es habitual en pipelines de validación, middleware HTTP, logging y sistemas de aprobaciones, donde se desea añadir o quitar responsabilidades sin modificar el código del emisor.
Sin este patrón, el emisor debe conocer a todos los posibles manejadores y decidir cuál utilizar, lo que genera condicionales complejos y dificulta la extensión. Agregar una nueva regla obliga a modificar al emisor o a crear jerarquías poco flexibles.
Chain of Responsibility encapsula cada responsabilidad en un handler autónomo que decide si procesa la solicitud o la delega. El emisor solo conoce al primer handler y el orden puede configurarse en tiempo de ejecución.
La intención del patrón es evitar acoplar el emisor con un receptor concreto otorgando la oportunidad de atender la solicitud a varios objetos. Es pertinente cuando:
La motivación principal es fomentar el principio Abierto/Cerrado y mantener los handlers cohesivos, cada uno enfocado en una tarea específica.
Los elementos claves son:
Opcionalmente se incluye un objeto builder o configurador que construye la cadena en función de parámetros externos.
Las cadenas pueden terminar de distintas maneras:
Elegir la modalidad adecuada evita duplicaciones y mantiene predecible el flujo de control.
Una empresa SaaS ofrece descuentos escalonados según el rol del usuario. Un agente comercial puede aprobar hasta el 5 %, un gerente hasta el 15 %, y porcentajes superiores requieren la validación de finanzas. Además, cada solicitud debe registrarse en un sistema de auditoría.
Chain of Responsibility permite encadenar handlers de auditoría, verificación de tope y autorización, agregando nuevos roles o reglas sin tocar el código del emisor que solicita el descuento.
El siguiente ejemplo combina autorizaciones escalonadas con registro obligatorio:
package tutorial.chain;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Objects;
public abstract class HandlerDescuento {
private HandlerDescuento siguiente;
public HandlerDescuento enlazar(HandlerDescuento siguiente) {
this.siguiente = siguiente;
return siguiente;
}
public final void procesar(SolicitudDescuento solicitud) {
if (!manejar(solicitud) && siguiente != null) {
siguiente.procesar(solicitud);
} else if (!solicitud.isAprobada() && siguiente == null) {
throw new IllegalStateException("Descuento no aprobado por ningún responsable");
}
}
protected abstract boolean manejar(SolicitudDescuento solicitud);
}
class HandlerAuditoria extends HandlerDescuento {
@Override
protected boolean manejar(SolicitudDescuento solicitud) {
System.out.printf("[%s] Auditoría: %s solicita %.2f%%%n",
Instant.now(), solicitud.getSolicitante(), solicitud.getPorcentaje());
return false;
}
}
class HandlerComercial extends HandlerDescuento {
private final BigDecimal limite = new BigDecimal("5");
@Override
protected boolean manejar(SolicitudDescuento solicitud) {
if (solicitud.getPorcentaje().compareTo(limite) <= 0) {
solicitud.aprobar("Comercial aprueba descuento");
return true;
}
return false;
}
}
class HandlerGerencia extends HandlerDescuento {
private final BigDecimal limite = new BigDecimal("15");
@Override
protected boolean manejar(SolicitudDescuento solicitud) {
if (solicitud.getPorcentaje().compareTo(limite) <= 0) {
solicitud.aprobar("Gerencia aprueba descuento");
return true;
}
return false;
}
}
class HandlerFinanzas extends HandlerDescuento {
@Override
protected boolean manejar(SolicitudDescuento solicitud) {
solicitud.aprobar("Finanzas autoriza descuento excepcional");
return true;
}
}
class SolicitudDescuento {
private final String solicitante;
private final BigDecimal porcentaje;
private boolean aprobada;
private String comentario;
SolicitudDescuento(String solicitante, BigDecimal porcentaje) {
this.solicitante = Objects.requireNonNull(solicitante);
this.porcentaje = Objects.requireNonNull(porcentaje);
}
public String getSolicitante() {
return solicitante;
}
public BigDecimal getPorcentaje() {
return porcentaje;
}
public boolean isAprobada() {
return aprobada;
}
public String getComentario() {
return comentario;
}
void aprobar(String comentario) {
this.aprobada = true;
this.comentario = comentario;
}
}
class ServicioDescuentos {
private final HandlerDescuento cadena;
ServicioDescuentos(HandlerDescuento cadena) {
this.cadena = cadena;
}
public SolicitudDescuento procesar(String solicitante, BigDecimal porcentaje) {
SolicitudDescuento solicitud = new SolicitudDescuento(solicitante, porcentaje);
cadena.procesar(solicitud);
return solicitud;
}
}
class AplicacionChain {
public static void main(String[] args) {
HandlerDescuento auditoria = new HandlerAuditoria();
HandlerDescuento comercial = auditoria.enlazar(new HandlerComercial());
HandlerDescuento gerencia = comercial.enlazar(new HandlerGerencia());
gerencia.enlazar(new HandlerFinanzas());
ServicioDescuentos servicio = new ServicioDescuentos(auditoria);
SolicitudDescuento solicitud = servicio.procesar("Laura", new BigDecimal("12"));
System.out.println("Resultado: " + solicitud.isAprobada() + " - " + solicitud.getComentario());
SolicitudDescuento solicitudExcepcional = servicio.procesar("Diego", new BigDecimal("25"));
System.out.println("Resultado: " + solicitudExcepcional.isAprobada() + " - " + solicitudExcepcional.getComentario());
}
}
La cadena comienza con auditoría, que siempre registra y delega. Luego responsable comercial y gerencia deciden si aprueban según su límite. Si ninguno acepta, finanzas actúa como último handler. El cliente solo conoce al primer nodo y puede reconfigurar la cadena sin alterar la lógica de negocio.
La documentación oficial de java.util.logging.Logger#addHandler muestra cómo el JDK encadena handlers de logging que reciben eventos en cascada. Frameworks web utilizan filtros y middleware que aplican este patrón para procesar peticiones HTTP, agregando autenticación, compresión o caché.
Algunas variantes comunes incluyen:
Un riesgo es construir cadenas excesivamente largas sin monitoreo, lo que dificulta diagnosticar fallos. También es problemático cuando un handler no delega por error, rompiendo el flujo y dejando solicitudes sin procesar. Otro anti-patrón es mezclar responsabilidades dispares en un mismo handler, perdiendo cohesión.
Chain of Responsibility se combina con Command para encapsular solicitudes y pasarlas por la cadena. Puede convivir con Decorator cuando se quiere agregar comportamiento alrededor de cada handler. Además, se complementa con Observer para notificar resultados de la cadena y con Strategy cuando la selección de cadena depende de políticas parametrizables.