patron-chain-responsibility

Patrón Chain of Responsibility: cadena de manejadores

  • 5 min

Chain of Responsibility es un patrón que pasa una petición por una cadena de manejadores hasta que uno la procesa o hasta que todos intervienen, según la variante.

Imaginad que trabajáis en una gran empresa y necesitáis aprobar la compra de un portátil nuevo de 2.000€.

  1. Se lo pides a tu jefe de equipo, que solo puede aprobar gastos de hasta 500 € y lo deriva al gerente.
  2. La petición llega al Gerente. Él dice: “Yo solo apruebo hasta 1.500€. Esto va para el Director”.
  3. Llega al Director. Él dice: “Vale, 2.000€ entra en mi presupuesto. Aprobado”.

Como cliente no tienes que perseguir a cada responsable. Entregas la solicitud al primer eslabón y la cadena decide cómo continuar.

El Problema: El condicional gigante

Sin este patrón, el código que decide quién procesa una petición suele convertirse en un monstruo lleno de if/else anidados.

// ❌ Código acoplado y difícil de mantener
public void ProcesarCompra(Compra compra)
{
    if (compra.Importe < 500)
    {
        // Lógica del Jefe de Equipo
    }
    else if (compra.Importe < 1500)
    {
        // Lógica del Gerente
    }
    else if (compra.Importe < 10000)
    {
        // Lógica del Director
    }
    else
    {
        throw new Exception("Nadie puede aprobar esto.");
    }
}
Copied!

El problema es evidente: el emisor de la petición conoce a todos los posibles receptores y la lógica exacta de decisión. Si cambiamos los límites de presupuesto o añadimos un nuevo cargo (ej: Vicepresidente), tenemos que tocar este código central.

La Solución: Pásalo al siguiente

El patrón propone convertir a cada manejador en una clase independiente. Cada manejador tiene dos opciones:

  1. Procesar la petición si puede.
  2. Pasar la petición al siguiente manejador de la cadena si no puede.

Implementación en C#

Vamos a implementar el sistema de aprobación de gastos corporativos.

El Manejador Abstracto

Esta clase base gestiona la “fontanería” de la cadena (guardar el siguiente y llamarlo).

public abstract class Aprobador
{
    protected Aprobador _siguiente;

    // Configura el siguiente eslabón y devuelve el siguiente 
    // (para poder encadenar configuraciones fluidamente)
    public Aprobador SetSiguiente(Aprobador siguiente)
    {
        _siguiente = siguiente;
        return siguiente;
    }

    // El método que decidirá
    public abstract void Procesar(Compra compra);
}

// Clase simple de datos
public class Compra
{
    public string Concepto { get; set; }
    public double Importe { get; set; }
}
Copied!

Los Eslabones de la Cadena

Cada clase solo se preocupa de su responsabilidad.

public class JefeEquipo : Aprobador
{
    public override void Procesar(Compra compra)
    {
        if (compra.Importe <= 500)
        {
            Console.WriteLine($"✅ Jefe de Equipo aprueba: {compra.Concepto} ({compra.Importe}€)");
        }
        else if (_siguiente != null)
        {
            Console.WriteLine("⏭️ Jefe de Equipo no puede. Pasando a Gerente...");
            _siguiente.Procesar(compra);
        }
    }
}

public class Gerente : Aprobador
{
    public override void Procesar(Compra compra)
    {
        if (compra.Importe <= 1500)
        {
            Console.WriteLine($"✅ Gerente aprueba: {compra.Concepto} ({compra.Importe}€)");
        }
        else if (_siguiente != null)
        {
            Console.WriteLine("⏭️ Gerente no puede. Pasando a Director...");
            _siguiente.Procesar(compra);
        }
    }
}

public class Director : Aprobador
{
    public override void Procesar(Compra compra)
    {
        if (compra.Importe <= 10000)
        {
            Console.WriteLine($"✅ Director aprueba: {compra.Concepto} ({compra.Importe}€)");
        }
        else
        {
            Console.WriteLine($"❌ Compra rechazada: {compra.Concepto}. Importe {compra.Importe}€ es demasiado alto.");
        }
    }
}
Copied!

El Cliente (Montando la cadena)

El cliente construye la cadena una vez y la usa las veces que quiera. Fíjate que el orden importa.

class Program
{
    static void Main(string[] args)
    {
        // 1. Crear los eslabones
        var jefe = new JefeEquipo();
        var gerente = new Gerente();
        var director = new Director();

        // 2. Unirlos: Jefe -> Gerente -> Director
        jefe.SetSiguiente(gerente).SetSiguiente(director);

        // 3. Probamos varios escenarios
        Console.WriteLine("\n--- Intento 1: Ratón (50€) ---");
        jefe.Procesar(new Compra { Concepto = "Ratón USB", Importe = 50 });

        Console.WriteLine("\n--- Intento 2: Portátil (1200€) ---");
        jefe.Procesar(new Compra { Concepto = "Portátil Dell", Importe = 1200 });

        Console.WriteLine("\n--- Intento 3: Servidor (6000€) ---");
        jefe.Procesar(new Compra { Concepto = "Servidor Rack", Importe = 6000 });
        
        Console.WriteLine("\n--- Intento 4: Yate (50000€) ---");
        jefe.Procesar(new Compra { Concepto = "Yate de empresa", Importe = 50000 });
    }
}
Copied!

Salida:

--- Intento 1: Ratón (50€) ---
✅ Jefe de Equipo aprueba: Ratón USB (50€)

--- Intento 2: Portátil (1200€) ---
⏭️ Jefe de Equipo no puede. Pasando a Gerente...
✅ Gerente aprueba: Portátil Dell (1200€)

--- Intento 3: Servidor (6000€) ---
⏭️ Jefe de Equipo no puede. Pasando a Gerente...
⏭️ Gerente no puede. Pasando a Director...
✅ Director aprueba: Servidor Rack (6000€)

--- Intento 4: Yate (50000€) ---
⏭️ Jefe de Equipo no puede. Pasando a Gerente...
⏭️ Gerente no puede. Pasando a Director...
❌ Compra rechazada: Yate de empresa. Importe 50000€ es demasiado alto.
Copied!

Variante: Middleware (ASP.NET Core)

Existe una variante moderna y muy popular de este patrón donde todos los eslabones pueden procesar parte de la petición antes de pasarla al siguiente. No se detiene en el primero que lo procesa.

Esto es exactamente lo que hace el pipeline de ASP.NET Core.

// Conceptualmente en ASP.NET Core
app.Use(async (context, next) =>
{
    Console.WriteLine("Middleware 1: Logueando petición");
    await next(); // Llama al siguiente
    Console.WriteLine("Middleware 1: Logueando respuesta");
});

app.Use(async (context, next) =>
{
    Console.WriteLine("Middleware 2: Autenticando");
    await next();
});
Copied!

Aquí la “cadena” permite añadir logging, autenticación, compresión y gestión de errores de forma modular.

¿Cuándo usar Chain of Responsibility?

  1. Múltiples manejadores: Cuando más de un objeto puede manejar una petición y no sabes a priori cuál será.
  2. Desacoplamiento: Cuando quieres enviar una petición a un objeto sin especificar explícitamente el receptor.
  3. Orden Dinámico: Cuando el conjunto de objetos que pueden manejar una petición debe especificarse dinámicamente (puedes cambiar el orden de la cadena en tiempo de ejecución).

Chain of Responsibility es el patrón de la burocracia eficiente. Nos permite construir flujos de decisión complejos conectando piezas simples. Si alguna vez veis un método con 5 if seguidos comprobando quién debe hacer el trabajo, convertidlo en una cadena.