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 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}");
}
}
}
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.
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.
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.