🚨 ¡Nueva review! ¡Mi teclado ideal! ⌨️ Perfecto para programar, el Logitech MX Keys S . ¡Échale un ojo! 👀

El patrón de diseño Chain of Responsibility

Pasa la patata caliente hasta que alguien se queme.

Escrito por domin el 23 de noviembre de 2025 · Actualizado el 15 de febrero de 2026

🔗 El patrón Chain of Responsibility: La cadena de mando

El patrón de diseño Chain of Responsibility, es un patrón de comportamiento para evitar acoplar el emisor de una petición a su receptor, dando a más de un objeto la oportunidad de manejarla.

¿Cómo puedes hacer que una solicitud sea atendida por el objeto adecuado sin que el emisor sepa quién es, y sin tener un if/else gigante y monstruoso?

La respuesta es encadenar los objetos receptores y pasar la solicitud a lo largo de la cadena hasta que un objeto la maneje.

Diagrama del Patrón Chain of Responsibility mostrando una cadena de manejadores.

🔗 1. Un ejemplo sencillo: El soporte técnico

Imagina que llamas a tu compañía de internet porque no te va el WiFi (La petición):

  1. Nivel 1 (robot): Te atiende una máquina. “¿Ha probado a reiniciar?”. Si no funciona, te pasa al siguiente nivel.
  2. Nivel 2 (operador humano): Te atiende una persona. Revisa tu factura. Si es un problema técnico complejo, te pasa al siguiente nivel.
  3. Nivel 3 (ingeniero): Te atiende un técnico especializado. Este sí sabe qué cable tocar.

Tú (el cliente) no sabes quién te va a resolver el problema. Solo llamas al primer número y la cadena se encarga de pasarte al nivel correcto. Eso es Chain of Responsibility.

¿Qué tipo de patrón de diseño es Chain of Responsibility?


🛠️ 2. Los roles del patrón

Para montar esta cadena, necesitamos tres piezas clave:

Handler (Manejador)

Define la interfaz para manejar peticiones. Contiene la referencia al siguiente manejador de la cadena (el puntero next).

Concrete Handlers

Son los que hacen el trabajo real. Comprueban si pueden manejar la petición. Si pueden, lo hacen. Si no, la pasan al siguiente.

Cliente

Inicia la petición en el primer eslabón de la cadena. No sabe ni le importa quién la va a resolver.

La gracia está en que cada manejador tiene solo dos opciones: yo me encargo o siguiente, por favor. No hay más. Y esa simplicidad es lo que hace el patrón tan potente.

¿Cuál es la principal ventaja frente a un switch/case gigante?


💾 3. Un ejemplo que seguro conoces: Middleware

Si has trabajado con Laravel, Symfony, Express o cualquier framework web moderno, ya has usado Chain of Responsibility sin saberlo. Los middlewares son exactamente este patrón:

  1. Middleware de Auth: ¿Está logueado? Si no, error 401. Si sí, pasa al siguiente.
  2. Middleware de Roles: ¿Tiene permisos? Si no, error 403. Si sí, pasa al siguiente.
  3. Middleware de Validación: ¿Los datos son correctos? Si no, error 422. Si sí, pasa al siguiente.
  4. Controlador: Procesa la petición y devuelve la respuesta.
// Así se ve en Laravel, por ejemplo
Route::middleware(['auth', 'role:admin', 'validate'])->group(function () {
    Route::get('/admin/dashboard', [AdminController::class, 'index']);
});

Cada middleware decide si la petición pasa o se detiene. El controlador no sabe qué middlewares hay delante de él, y los middlewares no saben qué controlador hay al final. Desacoplamiento total.


🧠 4. Ejemplo práctico en PHP

Vamos a implementar el sistema de soporte técnico del que hablábamos.

<?php

// 🔹 1. La Interfaz del Manejador (Handler)
abstract class SoporteHandler
{
    protected ?SoporteHandler $siguiente = null;

    public function setSiguiente(SoporteHandler $handler): SoporteHandler
    {
        $this->siguiente = $handler;
        // Devolvemos el handler para poder encadenar: $a->setSiguiente($b)->setSiguiente($c)
        return $handler;
    }

    public function manejar(string $problema): ?string
    {
        if ($this->siguiente) {
            return $this->siguiente->manejar($problema);
        }

        return null; // Nadie pudo atenderlo
    }
}

// 🔹 2. Manejadores Concretos
class Robot extends SoporteHandler
{
    public function manejar(string $problema): ?string
    {
        if ($problema === 'basico') {
            return "🤖 Robot: He reiniciado tu router. ¿Funciona? (Problema resuelto)";
        }
        echo "🤖 Robot: No sé resolver esto, paso al humano...\n";
        return parent::manejar($problema);
    }
}

class OperadorHumano extends SoporteHandler
{
    public function manejar(string $problema): ?string
    {
        if ($problema === 'facturacion') {
            return "👨‍💼 Operador: He corregido tu factura. (Problema resuelto)";
        }
        echo "👨‍💼 Operador: Esto es muy técnico, paso al ingeniero...\n";
        return parent::manejar($problema);
    }
}

class Ingeniero extends SoporteHandler
{
    public function manejar(string $problema): ?string
    {
        if ($problema === 'fuego') {
            return "👷 Ingeniero: He apagado el fuego del servidor. (Problema resuelto)";
        }
        echo "👷 Ingeniero: Ni yo sé qué pasa...\n";
        return parent::manejar($problema);
    }
}

// 🔹 3. Cliente (Configuración y Uso)
$robot = new Robot();
$operador = new OperadorHumano();
$ingeniero = new Ingeniero();

// Montamos la cadena: Robot -> Operador -> Ingeniero
$robot->setSiguiente($operador)->setSiguiente($ingeniero);

// Probamos
echo "--- Intento 1: Problema Básico ---\n";
echo $robot->manejar('basico') . "\n\n";

echo "--- Intento 2: Problema de Facturación ---\n";
echo $robot->manejar('facturacion') . "\n\n";

echo "--- Intento 3: Fuego en el servidor ---\n";
echo $robot->manejar('fuego') . "\n\n";

echo "--- Intento 4: Alienígenas ---\n";
$resultado = $robot->manejar('alienigenas');
if ($resultado === null) {
    echo "❌ Nadie pudo resolver el problema de los alienígenas.\n";
}

// 🖥️ Salida:
// --- Intento 1: Problema Básico ---
// 🤖 Robot: He reiniciado tu router. ¿Funciona? (Problema resuelto)
//
// --- Intento 2: Problema de Facturación ---
// 🤖 Robot: No sé resolver esto, paso al humano...
// 👨‍💼 Operador: He corregido tu factura. (Problema resuelto)
//
// --- Intento 3: Fuego en el servidor ---
// 🤖 Robot: No sé resolver esto, paso al humano...
// 👨‍💼 Operador: Esto es muy técnico, paso al ingeniero...
// 👷 Ingeniero: He apagado el fuego del servidor. (Problema resuelto)
//
// --- Intento 4: Alienígenas ---
// 🤖 Robot: No sé resolver esto, paso al humano...
// 👨‍💼 Operador: Esto es muy técnico, paso al ingeniero...
// 👷 Ingeniero: Ni yo sé qué pasa...
// ❌ Nadie pudo resolver el problema de los alienígenas.

Fíjate en varios detalles importantes:

¿Por qué setSiguiente() devuelve el handler que recibe como parámetro?

Añadir un nuevo nivel: el Director

Lo mejor de la cadena es que puedes añadir eslabones sin tocar nada de lo que ya existe:

class Director extends SoporteHandler
{
    public function manejar(string $problema): ?string
    {
        if ($problema === 'alienigenas') {
            return "🕴️ Director: He llamado a la NASA. (Problema resuelto)";
        }
        echo "🕴️ Director: Esto no es de mi competencia...\n";
        return parent::manejar($problema);
    }
}

// Ahora la cadena es: Robot -> Operador -> Ingeniero -> Director
$director = new Director();
$ingeniero->setSiguiente($director);

// Ahora sí se resuelve
echo $robot->manejar('alienigenas');
// 🤖 Robot: No sé resolver esto, paso al humano...
// 👨‍💼 Operador: Esto es muy técnico, paso al ingeniero...
// 👷 Ingeniero: Ni yo sé qué pasa...
// 🕴️ Director: He llamado a la NASA. (Problema resuelto)

Cero líneas modificadas en Robot, OperadorHumano o Ingeniero. OCP de SOLID cumplido.

¿Qué pasa si la petición llega al final de la cadena y nadie la ha manejado?


🔧 5. Otro ejemplo real: Validación encadenada

Un caso muy práctico es montar un sistema de validación donde cada validador comprueba una cosa:

abstract class ValidadorHandler
{
    protected ?ValidadorHandler $siguiente = null;

    public function setSiguiente(ValidadorHandler $handler): ValidadorHandler
    {
        $this->siguiente = $handler;
        return $handler;
    }

    public function validar(array $datos): ?string
    {
        if ($this->siguiente) {
            return $this->siguiente->validar($datos);
        }
        return null; // Todo OK, pasó todos los validadores
    }
}

class ValidarEmail extends ValidadorHandler
{
    public function validar(array $datos): ?string
    {
        if (empty($datos['email']) || !filter_var($datos['email'], FILTER_VALIDATE_EMAIL)) {
            return "❌ El email no es válido";
        }
        return parent::validar($datos);
    }
}

class ValidarPassword extends ValidadorHandler
{
    public function validar(array $datos): ?string
    {
        if (empty($datos['password']) || strlen($datos['password']) < 8) {
            return "❌ La contraseña debe tener al menos 8 caracteres";
        }
        return parent::validar($datos);
    }
}

class ValidarEdad extends ValidadorHandler
{
    public function validar(array $datos): ?string
    {
        if (empty($datos['edad']) || $datos['edad'] < 18) {
            return "❌ Debes ser mayor de edad";
        }
        return parent::validar($datos);
    }
}

// Montar la cadena
$validador = new ValidarEmail();
$validador->setSiguiente(new ValidarPassword())->setSiguiente(new ValidarEdad());

// Probar
$datos = ['email' => 'pepe@gmail.com', 'password' => '123', 'edad' => 25];
$error = $validador->validar($datos);

if ($error) {
    echo $error; // ❌ La contraseña debe tener al menos 8 caracteres
} else {
    echo "✅ Todo OK";
}

Cada validador se preocupa de una sola cosa. Si necesitas validar un nuevo campo, creas un validador nuevo y lo encadenas. Sin tocar los existentes.


🔀 6. Dos variantes del patrón

No todas las cadenas funcionan igual. Hay dos variantes principales:

Cadena pura

Solo uno maneja la petición. Cuando un manejador la resuelve, la cadena se detiene. Es el caso del soporte técnico: si el robot resuelve el problema, no pasa al operador.

Cadena de pipeline

Todos procesan la petición. Cada manejador hace su parte y pasa al siguiente. Es el caso del middleware: auth, validación, logging... todos actúan sobre la petición.

En la variante de pipeline, los manejadores no deciden si la petición es suya. Todos la procesan y la pasan. Solo detienen la cadena si encuentran un error (como un middleware de auth que rechaza al usuario).

¿Qué diferencia hay entre la variante 'cadena pura' y la variante 'pipeline'?


🔄 7. Comparativa con otros patrones

PatrónPropósitoDiferencia clave
Chain of ResponsibilityPasar una petición por una cadena hasta que alguien la manejeLa petición va en una sola dirección, de eslabón en eslabón
ObserverNotificar a múltiples objetos de un eventoTodos los observadores reciben la notificación, no uno solo
CommandEncapsular una acción como objetoEl Command sabe exactamente quién lo ejecuta, no hay cadena
StrategyIntercambiar algoritmos en runtimeElige un algoritmo, no recorre varios hasta encontrar el correcto

La diferencia más importante es que en Chain of Responsibility el emisor no sabe quién va a manejar la petición. En Command, Observer y Strategy el receptor está definido de antemano.


⚖️ 8. Relación con SOLID

SRP (Single Responsibility)

Cada manejador se encarga de una sola cosa. Robot resuelve problemas básicos, Ingeniero resuelve incendios. Cada uno tiene un solo motivo para cambiar.

OCP (Open/Closed)

Añadir un nuevo manejador = crear una clase nueva y encadenarla. Sin tocar el código existente. Lo hemos visto con el Director.

DIP (Dependency Inversion)

El cliente depende de la abstracción SoporteHandler, no de Robot o Ingeniero directamente. Y cada manejador conoce al siguiente como SoporteHandler, no como clase concreta.

LSP (Liskov Substitution)

Cualquier manejador concreto puede sustituir a otro en la cadena sin romper nada. Puedes reorganizar el orden de los eslabones sin problemas.

¿Qué principio SOLID cumple directamente al añadir un manejador nuevo sin tocar los existentes?


✅ 9. Ventajas y desventajas

Ventajas
  • Desacoplamiento: El emisor no necesita conocer la cadena ni quién resolverá su problema.
  • OCP: Añadir o quitar eslabones sin tocar código existente.
  • SRP: Cada manejador se ocupa de una sola cosa.
  • Flexible: Puedes reordenar la cadena dinámicamente en runtime.
  • Testeable: Cada manejador se puede testear de forma aislada.
Desventajas
  • Recepción no garantizada: La petición puede recorrer toda la cadena y no ser atendida.
  • Debug complicado: Seguir el flujo de una petición por 10 manejadores puede ser un lío.
  • Rendimiento: Si la cadena es muy larga, recorrerla entera puede ser costoso.
  • Orden importa: Si montas mal la cadena, las peticiones no se resuelven correctamente.

⚠️ 10. Errores comunes

1. No manejar el caso de “nadie resuelve”

Si la petición llega al final de la cadena y nadie la ha manejado, devuelve null. Si no controlas ese null, tu código va a petar en producción:

// ❌ MAL: no compruebas si alguien resolvió
$resultado = $robot->manejar('cosa_rara');
echo strtoupper($resultado); // TypeError: strtoupper(): Argument #1 must be of type string, null given

// ✅ BIEN: siempre comprueba
$resultado = $robot->manejar('cosa_rara');
if ($resultado === null) {
    echo "No se pudo resolver el problema";
}

Otra opción es tener un manejador por defecto al final de la cadena que siempre responda algo:

class ManejadorPorDefecto extends SoporteHandler
{
    public function manejar(string $problema): ?string
    {
        return "🤷 Lo sentimos, no hemos podido resolver su problema: $problema";
    }
}

// Lo pones al final
$ingeniero->setSiguiente(new ManejadorPorDefecto());

2. Cadenas infinitas

Si por error encadenas un manejador consigo mismo o creas un ciclo, tienes un bucle infinito:

// ❌ Esto es un bucle infinito
$robot->setSiguiente($operador);
$operador->setSiguiente($robot); // ¡Ciclo!

La cadena siempre debe ser lineal, sin ciclos. Si usas setSiguiente() como en el ejemplo, asegúrate de que no haya bucles.

3. Manejadores que hacen demasiado

Si un manejador tiene 200 líneas de código y maneja 5 tipos de problemas distintos, no es un manejador. Es un switch disfrazado. Cada manejador debería resolver una cosa:

// ❌ Esto es un switch disfrazado de Chain of Responsibility
class SuperManejador extends SoporteHandler
{
    public function manejar(string $problema): ?string
    {
        if ($problema === 'basico') { /* ... */ }
        if ($problema === 'facturacion') { /* ... */ }
        if ($problema === 'fuego') { /* ... */ }
        if ($problema === 'alienigenas') { /* ... */ }
        return parent::manejar($problema);
    }
}

¿Por qué es un error que un solo manejador tenga múltiples if/else para distintos tipos de problemas?

4. Olvidar devolver el handler en setSiguiente()

Si setSiguiente() no devuelve el handler, no puedes encadenar:

// ❌ Sin return, no puedes encadenar
public function setSiguiente(SoporteHandler $handler): void
{
    $this->siguiente = $handler;
}

// Tendrías que hacer esto:
$robot->setSiguiente($operador);
$operador->setSiguiente($ingeniero); // Dos líneas separadas

// ✅ Con return, encadenas en una línea
$robot->setSiguiente($operador)->setSiguiente($ingeniero);

🤔 11. ¿Cuándo usarlo y cuándo no?

Úsalo cuando...
  • Tienes varios objetos que pueden manejar una petición y no sabes cuál de antemano
  • Quieres que el emisor no conozca al receptor
  • Necesitas poder añadir o quitar manejadores dinámicamente
  • Estás implementando middleware, validación encadenada o flujos de aprobación
No lo uses cuando...
  • Sabes exactamente quién debe manejar cada petición (usa Strategy o Command)
  • Solo tienes 2-3 condiciones simples (un if/else basta)
  • Todos los manejadores necesitan procesar la petición (usa Observer)
  • El rendimiento es crítico y la cadena es muy larga

Regla de oro: Si te encuentras con un switch que crece cada semana y cada case podría ser una clase independiente, eso es Chain of Responsibility pidiéndote a gritos que lo uses.

¿Cuándo NO deberías usar Chain of Responsibility?


💡 12. Conclusión

El Chain of Responsibility es ideal para flujos de trabajo secuenciales donde varios objetos pueden tener algo que decir. Convierte un árbol de decisiones complejo en una lista ordenada y limpia de objetos que colaboran (o se pasan el marrón) entre sí.

Lo ves en todos los frameworks modernos como middleware. Lo ves en sistemas de soporte, validación, logging, filtros de spam… Donde hay una cadena de decisiones, hay un Chain of Responsibility escondido.

Cada eslabón solo sabe hacer su trabajo y pasar la bola si no le toca. No necesita conocer al resto de la cadena, eso es desacoplamiento.

EA, ¡saluditos y nos vemos en los bares! 🍻