24. Chain of Responsibility (Cadena de Responsabilidad) - Patrón de Comportamiento

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.

24.1 Problema y contexto

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.

24.2 Intención y motivació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:

  • Se requiere una secuencia flexible de validaciones o transformaciones.
  • Deseamos habilitar o deshabilitar reglas sin tocar el código que origina la petición.
  • La responsabilidad de la solicitud puede recaer en varios componentes según condiciones de negocio.
  • Es necesario combinar varias tareas (auditoría, monitoreo, autorización) alrededor de una operación central.

La motivación principal es fomentar el principio Abierto/Cerrado y mantener los handlers cohesivos, cada uno enfocado en una tarea específica.

24.3 Participantes y estructura

Los elementos claves son:

  • Handler: interfaz que declara una operación para procesar la solicitud y mantiene una referencia al siguiente handler.
  • ConcreteHandler: implementaciones que deciden si procesan la petición o la delegan.
  • Client: configura la cadena y dispara la solicitud.

Opcionalmente se incluye un objeto builder o configurador que construye la cadena en función de parámetros externos.

24.4 Modalidades de terminación

Las cadenas pueden terminar de distintas maneras:

  • Primera coincidencia: el primer handler que atiende la solicitud detiene la cadena.
  • Procesamiento acumulativo: todos los handlers ejecutan su lógica (por ejemplo, filtros que enriquecen un request).
  • Fallback: si ninguno atiende, se aplica un handler por defecto o se lanza una excepción.

Elegir la modalidad adecuada evita duplicaciones y mantiene predecible el flujo de control.

24.5 Escenario: aprobación de descuentos comerciales

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.

24.6 Implementación en Java

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());
    }
}

24.7 Explicación del flujo

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.

24.8 Cadena de responsabilidad en el ecosistema Java

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é.

24.9 Variantes y extensiones

Algunas variantes comunes incluyen:

  • Cadenas configurables: los handlers se cargan desde archivos de configuración o bases de datos.
  • Cadena circular: los handlers pueden reenviar solicitudes al inicio para reintentar, útil en procesos de corrección.
  • Cadena paralela: distribuye la carga dividiendo la solicitud en varias subcadenas para mejorar rendimiento.

24.10 Riesgos y malas prácticas

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.

24.11 Buenas prácticas para aplicar Chain of Responsibility

  • Nombrar claramente cada handler para transmitir su responsabilidad.
  • Registrar métricas (tiempo y cantidad de solicitudes atendidas) para detectar cuellos de botella.
  • Proveer un handler por defecto o fallback para manejar escenarios no contemplados.
  • Diseñar handlers inmutables o thread-safe cuando se utilicen en contextos concurrentes.

24.12 Relación con otros patrones

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.