Cada pedido que entra en PideYa debe superar una carrera de obstáculos antes de llegar a cocina: ¿huele a fraude?, ¿hay stock de todos los platos?, ¿repartimos en esa dirección?, ¿alcanza el importe mínimo del restaurante? Hoy esa carrera vive en un único método de validación que crece con cada control nuevo y que nadie puede reordenar, reutilizar ni probar por partes. Chain of Responsibility propone romperla en eslabones independientes: cada control es un objeto que decide si atiende la petición, la rechaza o la pasa al siguiente, y la cadena se monta —y se remonta— en ejecución.

Contenido

  1. El problema en PideYa: la validación monolítica del pedido entrante
  2. Intención y estructura del patrón
  3. Implementación Java completa
  4. Montar la cadena: configurable, no cableada
  5. Variantes: cortar al procesar vs. procesar y seguir
  6. La cadena en la vida real: filtros y middleware
  7. Cuándo usarlo y cuándo no
  8. Relación con otros patrones
  9. Errores comunes
  10. Ejercicios y conclusión

El problema en PideYa: la validación monolítica del pedido entrante

La FachadaCheckout del módulo 3 empieza su trabajo "validando" el pedido. Este es el interior de esa validación hoy:

public class ValidadorPedido {

    public void validar(Pedido pedido) {
        // 1. Control antifraude
        if (pedido.getCliente().tienePagosRechazadosRecientes()
                && pedido.getTotal().compareTo(new BigDecimal("100")) > 0) {
            throw new PedidoRechazadoException("Posible fraude");
        }
        // 2. Stock de todos los platos
        for (LineaPedido linea : pedido.getLineas()) {
            if (!servicioStock.hayStock(linea.getProducto())) {
                throw new PedidoRechazadoException("Sin stock: " + linea.getProducto());
            }
        }
        // 3. Zona de reparto
        if (!servicioZonas.cubre(pedido.getRestaurante(), pedido.getDireccionEntrega())) {
            throw new PedidoRechazadoException("Fuera de la zona de reparto");
        }
        // 4. Importe mínimo del restaurante
        if (pedido.getTotal().compareTo(pedido.getRestaurante().getImporteMinimo()) < 0) {
            throw new PedidoRechazadoException("No llega al importe mínimo");
        }
        // ... y creciendo: horario de apertura, límite de pedidos por franja...
    }
}

Funciona, pero fíjate en las fuerzas en tensión:

  • Cada control nuevo modifica la clase (adiós OCP); el método ya mezcla cuatro responsabilidades y sus cuatro dependencias (adiós SRP).
  • El orden está cableado. Negocio quiere probar si comprobar la zona antes que el stock reduce llamadas caras al servicio de inventario. Hoy eso es reescribir el método.
  • No hay reutilización. El control antifraude se quiere también en el alta de métodos de pago; el de zona, en la pantalla de carta. Hoy: copiar y pegar.
  • Configuración por contexto. Los pedidos de recogida en local no necesitan control de zona; los de empleados, ni fraude ni mínimo. Hoy: más if alrededor de los if.
  • Testear un control aislado exige montar el pedido perfecto que esquive los tres anteriores.

Formulado con precisión: hay una petición (el pedido entrante) y varios objetos que podrían tratarla, y no queremos que el emisor conozca cuántos son, quiénes son ni en qué orden actúan.

Intención y estructura del patrón

Intención (GoF): evitar acoplar el emisor de una petición a su receptor, dando a más de un objeto la oportunidad de tratarla. Encadenar los receptores y pasar la petición a lo largo de la cadena hasta que un objeto la trate.

Cada manejador conoce solo al siguiente eslabón. Recibe la petición y elige: la trata (y decide si la conversación termina) o la delega. El emisor solo conoce el primer eslabón.

classDiagram
    class ManejadorPedido {
        <<abstract>>
        -siguiente: ManejadorPedido
        +encadenar(siguiente: ManejadorPedido) ManejadorPedido
        +manejar(pedido: Pedido) ResultadoValidacion
        #comprobar(pedido: Pedido)* ResultadoValidacion
    }
    class ManejadorFraude {
        #comprobar(pedido: Pedido) ResultadoValidacion
    }
    class ManejadorStock {
        #comprobar(pedido: Pedido) ResultadoValidacion
    }
    class ManejadorZonaReparto {
        #comprobar(pedido: Pedido) ResultadoValidacion
    }
    class ManejadorImporteMinimo {
        #comprobar(pedido: Pedido) ResultadoValidacion
    }
    class FachadaCheckout {
        -cadenaValidacion: ManejadorPedido
    }

    ManejadorPedido <|-- ManejadorFraude
    ManejadorPedido <|-- ManejadorStock
    ManejadorPedido <|-- ManejadorZonaReparto
    ManejadorPedido <|-- ManejadorImporteMinimo
    ManejadorPedido o-- ManejadorPedido : siguiente
    FachadaCheckout --> ManejadorPedido : usa el primero
Rol GoF En PideYa
Handler (interfaz/base con la referencia al siguiente) ManejadorPedido
ConcreteHandler ManejadorFraude, ManejadorStock, ManejadorZonaReparto, ManejadorImporteMinimo
Client (emite la petición al primer eslabón) FachadaCheckout

La firma visual del patrón es esa autoasociación siguiente: un Handler que contiene un Handler. Compárala con Decorator, que también se encadena — volveremos a esa confusión clásica en la comparativa.

La conversación en el tiempo, para un pedido que cae en el tercer control:

sequenceDiagram
    participant C as FachadaCheckout
    participant F as ManejadorFraude
    participant S as ManejadorStock
    participant Z as ManejadorZonaReparto
    C->>F: manejar(pedido)
    F->>F: comprobar: OK
    F->>S: manejar(pedido)
    S->>S: comprobar: OK
    S->>Z: manejar(pedido)
    Z->>Z: comprobar: fuera de zona
    Z-->>C: rechazado("Fuera de la zona de reparto")
    Note over Z: el 4º eslabón ni se entera

Implementación Java completa

El resultado de la validación, mejor como valor que como excepción (las excepciones para el flujo normal de negocio son caras y ruidosas):

public record ResultadoValidacion(boolean aprobado, String motivo) {
    public static ResultadoValidacion ok() { return new ResultadoValidacion(true, null); }
    public static ResultadoValidacion rechazo(String motivo) {
        return new ResultadoValidacion(false, motivo);
    }
}

El Handler base concentra la fontanería de la cadena, con el truco de plantilla que hace triviales los eslabones concretos:

public abstract class ManejadorPedido {

    private ManejadorPedido siguiente;

    /** Devuelve el eslabón añadido para poder encadenar con fluidez. */
    public ManejadorPedido encadenar(ManejadorPedido siguiente) {
        this.siguiente = siguiente;
        return siguiente;
    }

    /** Fontanería común: comprobar este eslabón y, si aprueba, delegar. */
    public ResultadoValidacion manejar(Pedido pedido) {
        ResultadoValidacion resultado = comprobar(pedido);
        if (!resultado.aprobado()) {
            return resultado;              // corta la cadena: rechazo definitivo
        }
        if (siguiente == null) {
            return ResultadoValidacion.ok(); // fin de la cadena: todo aprobado
        }
        return siguiente.manejar(pedido);  // delega en el siguiente
    }

    /** Lo único que implementa cada eslabón concreto. */
    protected abstract ResultadoValidacion comprobar(Pedido pedido);
}

Dos detalles que conviene explicar:

  • manejar es público y final en la práctica: define el protocolo (comprobar → cortar o delegar) una sola vez. Los eslabones solo rellenan comprobar. (Este "método público que fija el flujo + método protegido que varía" es un anticipo en miniatura de Template Method.)
  • El corte es explícito: un rechazo no sigue cadena abajo. Es la variante clásica; veremos la otra enseguida.

Los eslabones concretos — cada uno pequeño, con una sola responsabilidad y sus propias dependencias inyectadas (DIP):

public class ManejadorFraude extends ManejadorPedido {

    private final ServicioAntifraude antifraude;

    public ManejadorFraude(ServicioAntifraude antifraude) {
        this.antifraude = antifraude;
    }

    @Override
    protected ResultadoValidacion comprobar(Pedido pedido) {
        return antifraude.esSospechoso(pedido)
                ? ResultadoValidacion.rechazo("Posible fraude")
                : ResultadoValidacion.ok();
    }
}

public class ManejadorImporteMinimo extends ManejadorPedido {

    @Override
    protected ResultadoValidacion comprobar(Pedido pedido) {
        BigDecimal minimo = pedido.getRestaurante().getImporteMinimo();
        return pedido.getTotal().compareTo(minimo) < 0
                ? ResultadoValidacion.rechazo("No llega al importe mínimo de " + minimo + " €")
                : ResultadoValidacion.ok();
    }
}

(ManejadorStock y ManejadorZonaReparto siguen el mismo molde con sus servicios respectivos.)

Montar la cadena: configurable, no cableada

El punto donde este patrón paga el peaje: alguien tiene que montar la cadena. Ese alguien es código de configuración (composición root, o una fábrica como las del módulo 2), nunca los eslabones:

ManejadorPedido cadena = new ManejadorFraude(antifraude);
cadena.encadenar(new ManejadorZonaReparto(servicioZonas)) // zona antes que stock:
      .encadenar(new ManejadorStock(servicioStock))       // ahorra llamadas caras
      .encadenar(new ManejadorImporteMinimo());

FachadaCheckout checkout = new FachadaCheckout(cadena, /* ... */);

Y aquí cobran vida las fuerzas que el monolito no podía atender:

  • Reordenar es reordenar líneas de configuración.
  • Cadenas por contexto: el pedido de recogida en local monta Fraude → Stock → ImporteMinimo (sin zona); el interno de empleados, solo Stock. Mismos eslabones, cadenas distintas.
  • Testear un eslabón es instanciarlo suelto, sin cadena, y llamar a comprobar.

Variantes: cortar al procesar vs. procesar y seguir

La variante GoF pura es "la trata exactamente uno": la petición avanza hasta que un manejador la atiende, y ahí muere (piensa en el escalado de incidencias de soporte: el bot resuelve las preguntas frecuentes; lo que no sabe, lo pasa al agente humano; lo que el agente no puede, al supervisor — cada incidencia la resuelve un nivel). Nuestra validación usa la variante complementaria, igual de común: "todos opinan hasta que uno vete". Y existe una tercera: "todos procesan siempre", donde cada eslabón hace su parte y delega incondicionalmente.

Variante Quién procesa Cuándo se corta Ejemplo
Primer competente Exactamente uno Al primero que atiende Escalado de soporte: bot → agente → supervisor
Todos hasta veto Todos los anteriores al fallo Al primer rechazo Nuestra validación de pedidos
Tubería completa Todos Nunca (salvo error) Filtros que enriquecen la petición por etapas

En las tres, la esencia es idéntica: emisor desacoplado de receptores, receptores desacoplados entre sí, cadena montada en configuración. Dos avisos honestos: en la variante GoF pura nadie garantiza que la petición sea atendida —puede caerse por el final de la cadena, y hay que decidir qué pasa entonces (valor por defecto, excepción, registro en RegistroEventos)—; y en la de veto, un eslabón que olvide delegar rompe silenciosamente todos los controles posteriores (por eso concentramos la delegación en la base).

La cadena en la vida real: filtros y middleware

Si has tocado desarrollo web, ya has usado este patrón sin saberlo:

  • Los filtros de Servlet (jakarta.servlet.Filter): cada filtro recibe la petición y un FilterChain, hace su parte (autenticación, compresión, CORS...) y decide si llama a chain.doFilter(...) o corta.
  • El middleware de Express, ASP.NET Core o los interceptores de Spring: misma idea, cada pieza procesa y llama a next().
  • El event bubbling del DOM: el clic sube por la jerarquía de elementos hasta que alguno lo maneja.

La lección de estos ejemplos es doble: el patrón está vivísimo (aunque casi nunca lo llamen por su nombre GoF), y su hábitat natural son las tuberías de procesamiento configurables, más que el "exactamente uno responde" del libro original.

Cuándo usarlo y cuándo no

Úsalo cuando:

  • Más de un objeto puede tratar una petición y el emisor no debe saber cuál.
  • Quieres añadir, quitar o reordenar pasos de procesamiento sin tocar los existentes ni el emisor.
  • El conjunto de manejadores debe variar por contexto o configuración (incluso en ejecución).

Evítalo cuando:

  • Hay un solo receptor fijo: una llamada directa es más simple y más clara. La cadena de un eslabón es sobreingeniería de manual (lección 01-06).
  • El emisor necesita garantía de tratamiento y respuesta inmediata de un responsable concreto: la cadena introduce incertidumbre sobre quién (y si alguien) responderá.
  • El orden entre pasos esconde dependencias de datos fuertes (cada paso necesita resultados del anterior): eso es una tubería con contrato entre etapas, y quizá un método secuencial claro sea más honesto que una pseudo-cadena rígida.

Coste a pagar: la traza de ejecución se fragmenta (para saber qué controles se aplican a un pedido hay que mirar la configuración, no un método), y depurar "¿por qué se rechazó?" obliga a recorrer eslabones. Un buen ResultadoValidacion con motivo, y logging en la base con RegistroEventos, mitigan mucho.

Relación con otros patrones

Solo menciones; cada cosa en su lección:

  • Decorator: mecánica casi idéntica (objetos enlazados que delegan), intención opuesta — el decorador siempre delega y añade; el manejador decide si atender o pasar. Careo completo en la comparativa.
  • Command: lo que viaja por la cadena puede ser un comando; GoF los combina a menudo (la petición reificada busca su manejador).
  • Composite: en un árbol, el padre de cada nodo es un "siguiente" natural — las peticiones pueden escalar hacia la raíz.
  • Facade: en PideYa, la fachada de checkout es el cliente de la cadena: la dispara sin conocer sus eslabones.

Errores comunes

  • Olvidar delegar al siguiente en un eslabón concreto (en variantes donde cada concreto gestiona la llamada): los controles posteriores desaparecen sin ruido. Antídoto: fontanería de delegación en la clase base, como hicimos.
  • Cadena sin final definido: en la variante "primer competente", no decidir qué ocurre si nadie atiende. Define siempre el comportamiento por defecto (manejador terminal, valor por defecto o excepción explícita).
  • Eslabones con estado mutable compartido entre peticiones: la misma cadena suele servir peticiones concurrentes; los manejadores deben ser sin estado (como los nuestros: solo dependencias inmutables).
  • Meter el montaje de la cadena dentro de los eslabones (cada uno crea a su siguiente con new): resucita el acoplamiento que veníamos a matar. El montaje es de la configuración.
  • Cadenas kilométricas para lógica trivial: cuatro if seguidos que nunca cambiarán no necesitan cuatro clases. El patrón se gana el pan cuando hay variabilidad real (orden, contexto, reutilización).

Ejercicios

Ejercicio 1: nuevo eslabón sin tocar nada

El negocio pide un control nuevo: rechazar pedidos si el restaurante está cerrado en la franja horaria solicitada (pedido.getFranjaEntrega(), restaurante.abiertoEn(franja)). Escribe ManejadorHorario e indica dónde lo colocarías en la cadena y por qué.

Ejercicio 2: cadena de escalado de soporte

Modela con la variante "primer competente" el escalado de incidencias: BotSoporte resuelve si la incidencia es de tipo PREGUNTA_FRECUENTE; AgenteSoporte resuelve si importeAfectado <= 50; SupervisorSoporte resuelve todo lo demás. Diseña la base ManejadorIncidencia (¿en qué cambia su fontanería respecto a ManejadorPedido?) y el eslabón BotSoporte.

Ejercicio 3: detectar el antipatrón

Un compañero implementa ManejadorStock así: if (!hayStock) return rechazo(...); else return new ManejadorZonaReparto(zonas).manejar(pedido);. Señala los dos problemas de diseño.

Soluciones

Solución 1:

public class ManejadorHorario extends ManejadorPedido {

    @Override
    protected ResultadoValidacion comprobar(Pedido pedido) {
        return pedido.getRestaurante().abiertoEn(pedido.getFranjaEntrega())
                ? ResultadoValidacion.ok()
                : ResultadoValidacion.rechazo("Restaurante cerrado en esa franja");
    }
}

Colocación razonable: al principio (tras fraude o incluso antes): es una comprobación local y baratísima que puede ahorrar llamadas caras a stock y zonas. Lo importante del ejercicio: añadirlo no toca ni la base, ni los demás eslabones, ni la fachada — solo una línea en la configuración. OCP en acción.

Solución 2: la fontanería cambia en la condición de corte — se corta cuando alguien atiende, no cuando alguien rechaza; y si nadie atiende, hay que decidir el final (aquí, el supervisor es terminal, pero la base debe protegerse igualmente):

public abstract class ManejadorIncidencia {

    private ManejadorIncidencia siguiente;

    public ManejadorIncidencia encadenar(ManejadorIncidencia s) {
        this.siguiente = s;
        return s;
    }

    public Resolucion manejar(Incidencia incidencia) {
        if (puedeResolver(incidencia)) {
            return resolver(incidencia);            // primer competente: aquí termina
        }
        if (siguiente == null) {
            throw new IllegalStateException("Incidencia sin responsable: " + incidencia.getId());
        }
        return siguiente.manejar(incidencia);       // escalar
    }

    protected abstract boolean puedeResolver(Incidencia incidencia);
    protected abstract Resolucion resolver(Incidencia incidencia);
}

public class BotSoporte extends ManejadorIncidencia {
    @Override protected boolean puedeResolver(Incidencia i) {
        return i.getTipo() == TipoIncidencia.PREGUNTA_FRECUENTE;
    }
    @Override protected Resolucion resolver(Incidencia i) {
        return Resolucion.automatica(respuestas.buscar(i));
    }
}

Solución 3: (1) el eslabón crea con new a su siguiente, cableando el orden dentro del manejador — la cadena deja de ser configurable y ManejadorStock se acopla a ManejadorZonaReparto y a sus dependencias; (2) duplica la fontanería de delegación que ya vive en la base, así que cualquier cambio de protocolo (logging, métricas) habrá que replicarlo eslabón a eslabón. La delegación es de la base; el montaje, de la configuración.

Conclusión

Chain of Responsibility convierte una secuencia monolítica de controles en eslabones autónomos: cada ManejadorPedido hace una sola comprobación, ignora a sus vecinos y la cadena completa se decide en configuración — reordenable, recortable por contexto y testeable pieza a pieza. Has visto sus tres variantes (primer competente, veto, tubería), su presencia ubicua en filtros y middleware, y su precio: la traza fragmentada y la responsabilidad de definir qué pasa al final de la cadena.

Por la cadena ha viajado el pedido; el siguiente patrón reifica el viaje mismo. En el panel del restaurante de PideYa, "aceptar pedido", "marcar en preparación" o "cancelar" son hoy llamadas que se ejecutan y se esfuman: no se pueden encolar, ni registrar, ni —lo que más pide el restaurante— deshacer. Para eso hay que convertir cada petición en un objeto con vida propia. Nos vemos en Command.

Curso de Patrones de Diseño de Software

Módulo 1: Introducción a los Patrones de Diseño

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados