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
- El problema en PideYa: la validación monolítica del pedido entrante
- Intención y estructura del patrón
- Implementación Java completa
- Montar la cadena: configurable, no cableada
- Variantes: cortar al procesar vs. procesar y seguir
- La cadena en la vida real: filtros y middleware
- Cuándo usarlo y cuándo no
- Relación con otros patrones
- Errores comunes
- 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
ifalrededor de losif. - 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:
manejares público y final en la práctica: define el protocolo (comprobar → cortar o delegar) una sola vez. Los eslabones solo rellenancomprobar. (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, soloStock. 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 unFilterChain, hace su parte (autenticación, compresión, CORS...) y decide si llama achain.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
ifseguidos 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
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
