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 C#

El siguiente ejemplo combina autorizaciones escalonadas con registro obligatorio y aprovecha tipos propios de .NET:

using System;

namespace Tutorial.ChainOfResponsibility
{
    public interface IHandlerDescuento
    {
        IHandlerDescuento Enlazar(IHandlerDescuento siguiente);
        void Procesar(SolicitudDescuento solicitud);
    }

    public abstract class HandlerDescuentoBase : IHandlerDescuento
    {
        private IHandlerDescuento _siguiente;

        public IHandlerDescuento Enlazar(IHandlerDescuento siguiente)
        {
            _siguiente = siguiente ?? throw new ArgumentNullException(nameof(siguiente));
            return siguiente;
        }

        public void Procesar(SolicitudDescuento solicitud)
        {
            if (solicitud == null) throw new ArgumentNullException(nameof(solicitud));

            bool atendida = Manejar(solicitud);
            if (!atendida)
            {
                if (_siguiente != null)
                {
                    _siguiente.Procesar(solicitud);
                }
                else if (!solicitud.Aprobada)
                {
                    throw new InvalidOperationException("Descuento no aprobado por ningun responsable.");
                }
            }
        }

        protected abstract bool Manejar(SolicitudDescuento solicitud);
    }

    public sealed class HandlerAuditoria : HandlerDescuentoBase
    {
        protected override bool Manejar(SolicitudDescuento solicitud)
        {
            Console.WriteLine($"[{DateTimeOffset.UtcNow:O}] Auditoria: {solicitud.Solicitante} solicita {solicitud.Porcentaje:0.##}%");
            return false;
        }
    }

    public sealed class HandlerComercial : HandlerDescuentoBase
    {
        private const decimal Limite = 5m;

        protected override bool Manejar(SolicitudDescuento solicitud)
        {
            if (solicitud.Porcentaje <= Limite)
            {
                solicitud.Aprobar("Comercial aprueba descuento");
                return true;
            }

            return false;
        }
    }

    public sealed class HandlerGerencia : HandlerDescuentoBase
    {
        private const decimal Limite = 15m;

        protected override bool Manejar(SolicitudDescuento solicitud)
        {
            if (solicitud.Porcentaje <= Limite)
            {
                solicitud.Aprobar("Gerencia aprueba descuento");
                return true;
            }

            return false;
        }
    }

    public sealed class HandlerFinanzas : HandlerDescuentoBase
    {
        protected override bool Manejar(SolicitudDescuento solicitud)
        {
            solicitud.Aprobar("Finanzas autoriza descuento excepcional");
            return true;
        }
    }

    public sealed class SolicitudDescuento
    {
        public SolicitudDescuento(string solicitante, decimal porcentaje)
        {
            Solicitante = solicitante ?? throw new ArgumentNullException(nameof(solicitante));
            if (porcentaje < 0m)
            {
                throw new ArgumentOutOfRangeException(nameof(porcentaje), "El porcentaje debe ser positivo.");
            }

            Porcentaje = porcentaje;
        }

        public string Solicitante { get; }
        public decimal Porcentaje { get; }
        public bool Aprobada { get; private set; }
        public string Comentario { get; private set; }

        public void Aprobar(string comentario)
        {
            Aprobada = true;
            Comentario = comentario;
        }
    }

    public sealed class ServicioDescuentos
    {
        private readonly IHandlerDescuento _cadena;

        public ServicioDescuentos(IHandlerDescuento cadena)
        {
            _cadena = cadena ?? throw new ArgumentNullException(nameof(cadena));
        }

        public SolicitudDescuento Procesar(string solicitante, decimal porcentaje)
        {
            var solicitud = new SolicitudDescuento(solicitante, porcentaje);
            _cadena.Procesar(solicitud);
            return solicitud;
        }
    }

    public static class AplicacionChain
    {
        public static void Main()
        {
            IHandlerDescuento auditoria = new HandlerAuditoria();
            IHandlerDescuento comercial = auditoria.Enlazar(new HandlerComercial());
            IHandlerDescuento gerencia = comercial.Enlazar(new HandlerGerencia());
            gerencia.Enlazar(new HandlerFinanzas());

            var servicio = new ServicioDescuentos(auditoria);

            SolicitudDescuento solicitud = servicio.Procesar("Laura", 12m);
            Console.WriteLine($"Resultado: {solicitud.Aprobada} - {solicitud.Comentario}");

            SolicitudDescuento solicitudExcepcional = servicio.Procesar("Diego", 25m);
            Console.WriteLine($"Resultado: {solicitudExcepcional.Aprobada} - {solicitudExcepcional.Comentario}");
        }
    }
}

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

ASP.NET Core implementa middleware encadenado que procesa solicitudes HTTP en etapas, permitiendo agregar autenticación, logging o compresión sin acoplar la aplicación al pipeline concreto. Bibliotecas como MediatR y Stateless ofrecen comportamientos similares con pipeline behaviors y reglas configurables que se inspiran en Chain of Responsibility.

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.