Cada comanda que entra a PideYa ha de superar una cursa d'obstacles abans d'arribar a cuina: fa olor de frau?, hi ha estoc de tots els plats?, repartim en aquella adreça?, arriba a la quantia mínima del restaurant? Avui aquesta cursa viu en un únic mètode de validació que creix amb cada control nou i que ningú no pot reordenar, reutilitzar ni provar per parts. Chain of Responsibility proposa trencar-la en baules independents: cada control és un objecte que decideix si atén la petició, la rebutja o la passa al següent, i la cadena es munta —i es remunta— en execució.
Contingut
- El problema a PideYa: la validació monolítica de la comanda entrant
- Intenció i estructura del patró
- Implementació Java completa
- Muntar la cadena: configurable, no cablejada
- Variants: tallar en processar vs. processar i seguir
- La cadena a la vida real: filtres i middleware
- Quan usar-lo i quan no
- Relació amb altres patrons
- Errors comuns
- Exercicis i conclusió
El problema a PideYa: la validació monolítica de la comanda entrant
La FacanaCheckout del mòdul 3 comença la seva feina "validant" la comanda. Aquest és l'interior d'aquesta validació avui:
public class ValidadorComanda {
public void validar(Comanda comanda) {
// 1. Control antifrau
if (comanda.getClient().tePagamentsRebutjatsRecents()
&& comanda.getTotal().compareTo(new BigDecimal("100")) > 0) {
throw new ComandaRebutjadaException("Possible frau");
}
// 2. Estoc de tots els plats
for (LiniaComanda linia : comanda.getLinies()) {
if (!serveiEstoc.hiHaEstoc(linia.getProducte())) {
throw new ComandaRebutjadaException("Sense estoc: " + linia.getProducte());
}
}
// 3. Zona de repartiment
if (!serveiZones.cobreix(comanda.getRestaurant(), comanda.getAdrecaLliurament())) {
throw new ComandaRebutjadaException("Fora de la zona de repartiment");
}
// 4. Quantia minima del restaurant
if (comanda.getTotal().compareTo(comanda.getRestaurant().getQuantiaMinima()) < 0) {
throw new ComandaRebutjadaException("No arriba a la quantia minima");
}
// ... i creixent: horari d'obertura, limit de comandes per franja...
}
}Funciona, però fixa't en les forces en tensió:
- Cada control nou modifica la classe (adeu OCP); el mètode ja barreja quatre responsabilitats i les seves quatre dependències (adeu SRP).
- L'ordre està cablejat. Negoci vol provar si comprovar la zona abans que l'estoc redueix crides cares al servei d'inventari. Avui això és reescriure el mètode.
- No hi ha reutilització. El control antifrau es vol també a l'alta de mètodes de pagament; el de zona, a la pantalla de carta. Avui: copiar i enganxar.
- Configuració per context. Les comandes de recollida al local no necessiten control de zona; les d'empleats, ni frau ni mínim. Avui: més
ifal voltant delsif. - Testejar un control aïllat exigeix muntar la comanda perfecta que esquivi els tres anteriors.
Formulat amb precisió: hi ha una petició (la comanda entrant) i diversos objectes que podrien tractar-la, i no volem que l'emissor conegui quants són, qui són ni en quin ordre actuen.
Intenció i estructura del patró
Intenció (GoF): evitar acoblar l'emissor d'una petició al seu receptor, donant a més d'un objecte l'oportunitat de tractar-la. Encadenar els receptors i passar la petició al llarg de la cadena fins que un objecte la tracti.
Cada manipulador coneix només la següent baula. Rep la petició i tria: la tracta (i decideix si la conversa acaba) o la delega. L'emissor només coneix la primera baula.
classDiagram
class GestorComanda {
<<abstract>>
-seguent: GestorComanda
+encadenar(seguent: GestorComanda) GestorComanda
+gestionar(comanda: Comanda) ResultatValidacio
#comprovar(comanda: Comanda)* ResultatValidacio
}
class GestorFrau {
#comprovar(comanda: Comanda) ResultatValidacio
}
class GestorEstoc {
#comprovar(comanda: Comanda) ResultatValidacio
}
class GestorZonaRepartiment {
#comprovar(comanda: Comanda) ResultatValidacio
}
class GestorQuantiaMinima {
#comprovar(comanda: Comanda) ResultatValidacio
}
class FacanaCheckout {
-cadenaValidacio: GestorComanda
}
GestorComanda <|-- GestorFrau
GestorComanda <|-- GestorEstoc
GestorComanda <|-- GestorZonaRepartiment
GestorComanda <|-- GestorQuantiaMinima
GestorComanda o-- GestorComanda : seguent
FacanaCheckout --> GestorComanda : usa el primer
| Rol GoF | A PideYa |
|---|---|
| Handler (interfície/base amb la referència al següent) | GestorComanda |
| ConcreteHandler | GestorFrau, GestorEstoc, GestorZonaRepartiment, GestorQuantiaMinima |
| Client (emet la petició a la primera baula) | FacanaCheckout |
La firma visual del patró és aquesta autoassociació seguent: un Handler que conté un Handler. Compara-la amb Decorator, que també s'encadena — tornarem a aquesta confusió clàssica a la comparativa.
La conversa en el temps, per a una comanda que cau al tercer control:
sequenceDiagram
participant C as FacanaCheckout
participant F as GestorFrau
participant S as GestorEstoc
participant Z as GestorZonaRepartiment
C->>F: gestionar(comanda)
F->>F: comprovar: OK
F->>S: gestionar(comanda)
S->>S: comprovar: OK
S->>Z: gestionar(comanda)
Z->>Z: comprovar: fora de zona
Z-->>C: rebutjada("Fora de la zona de repartiment")
Note over Z: la 4a baula ni se n'assabenta
Implementació Java completa
El resultat de la validació, millor com a valor que com a excepció (les excepcions per al flux normal de negoci són cares i sorolloses):
public record ResultatValidacio(boolean aprovada, String motiu) {
public static ResultatValidacio ok() { return new ResultatValidacio(true, null); }
public static ResultatValidacio rebuig(String motiu) {
return new ResultatValidacio(false, motiu);
}
}El Handler base concentra la fontaneria de la cadena, amb el truc de plantilla que fa trivials les baules concretes:
public abstract class GestorComanda {
private GestorComanda seguent;
/** Retorna la baula afegida per poder encadenar amb fluidesa. */
public GestorComanda encadenar(GestorComanda seguent) {
this.seguent = seguent;
return seguent;
}
/** Fontaneria comuna: comprovar aquesta baula i, si aprova, delegar. */
public ResultatValidacio gestionar(Comanda comanda) {
ResultatValidacio resultat = comprovar(comanda);
if (!resultat.aprovada()) {
return resultat; // talla la cadena: rebuig definitiu
}
if (seguent == null) {
return ResultatValidacio.ok(); // fi de la cadena: tot aprovat
}
return seguent.gestionar(comanda); // delega en el seguent
}
/** L'unica cosa que implementa cada baula concreta. */
protected abstract ResultatValidacio comprovar(Comanda comanda);
}Dos detalls que convé explicar:
gestionarés públic i final a la pràctica: defineix el protocol (comprovar → tallar o delegar) una sola vegada. Les baules només omplencomprovar. (Aquest "mètode públic que fixa el flux + mètode protegit que varia" és un avançament en miniatura de Template Method.)- El tall és explícit: un rebuig no continua cadena avall. És la variant clàssica; veurem l'altra de seguida.
Les baules concretes — cadascuna petita, amb una sola responsabilitat i les seves pròpies dependències injectades (DIP):
public class GestorFrau extends GestorComanda {
private final ServeiAntifrau antifrau;
public GestorFrau(ServeiAntifrau antifrau) {
this.antifrau = antifrau;
}
@Override
protected ResultatValidacio comprovar(Comanda comanda) {
return antifrau.esSospitosa(comanda)
? ResultatValidacio.rebuig("Possible frau")
: ResultatValidacio.ok();
}
}
public class GestorQuantiaMinima extends GestorComanda {
@Override
protected ResultatValidacio comprovar(Comanda comanda) {
BigDecimal minima = comanda.getRestaurant().getQuantiaMinima();
return comanda.getTotal().compareTo(minima) < 0
? ResultatValidacio.rebuig("No arriba a la quantia minima de " + minima + " €")
: ResultatValidacio.ok();
}
}(GestorEstoc i GestorZonaRepartiment segueixen el mateix motlle amb els seus serveis respectius.)
Muntar la cadena: configurable, no cablejada
El punt on aquest patró paga el peatge: algú ha de muntar la cadena. Aquest algú és codi de configuració (composition root, o una fàbrica com les del mòdul 2), mai les baules:
GestorComanda cadena = new GestorFrau(antifrau);
cadena.encadenar(new GestorZonaRepartiment(serveiZones)) // zona abans que estoc:
.encadenar(new GestorEstoc(serveiEstoc)) // estalvia crides cares
.encadenar(new GestorQuantiaMinima());
FacanaCheckout checkout = new FacanaCheckout(cadena, /* ... */);I aquí prenen vida les forces que el monòlit no podia atendre:
- Reordenar és reordenar línies de configuració.
- Cadenes per context: la comanda de recollida al local munta
Frau → Estoc → QuantiaMinima(sense zona); la interna d'empleats, nomésEstoc. Mateixes baules, cadenes diferents. - Testejar una baula és instanciar-la solta, sense cadena, i cridar
comprovar.
Variants: tallar en processar vs. processar i seguir
La variant GoF pura és "la tracta exactament un": la petició avança fins que un manipulador l'atén, i allà mor (pensa en l'escalat d'incidències de suport: el bot resol les preguntes freqüents; el que no sap, ho passa a l'agent humà; el que l'agent no pot, al supervisor — cada incidència la resol un nivell). La nostra validació usa la variant complementària, igual de comuna: "tots opinen fins que un veta". I n'existeix una tercera: "tots processen sempre", on cada baula fa la seva part i delega incondicionalment.
| Variant | Qui processa | Quan es talla | Exemple |
|---|---|---|---|
| Primer competent | Exactament un | Al primer que atén | Escalat de suport: bot → agent → supervisor |
| Tots fins al veto | Tots els anteriors a la fallada | Al primer rebuig | La nostra validació de comandes |
| Canonada completa | Tots | Mai (llevat d'error) | Filtres que enriqueixen la petició per etapes |
En les tres, l'essència és idèntica: emissor desacoblat de receptors, receptors desacoblats entre si, cadena muntada en configuració. Dos avisos honestos: en la variant GoF pura ningú no garanteix que la petició sigui atesa —pot caure pel final de la cadena, i cal decidir què passa llavors (valor per defecte, excepció, registre a RegistreEsdeveniments)—; i en la de veto, una baula que oblidi delegar trenca silenciosament tots els controls posteriors (per això concentrem la delegació a la base).
La cadena a la vida real: filtres i middleware
Si has tocat desenvolupament web, ja has usat aquest patró sense saber-ho:
- Els filtres de Servlet (
jakarta.servlet.Filter): cada filtre rep la petició i unFilterChain, fa la seva part (autenticació, compressió, CORS...) i decideix si cridachain.doFilter(...)o talla. - El middleware d'Express, ASP.NET Core o els interceptors de Spring: mateixa idea, cada peça processa i crida
next(). - L'event bubbling del DOM: el clic puja per la jerarquia d'elements fins que algun el gestiona.
La lliçó d'aquests exemples és doble: el patró està vivíssim (encara que gairebé mai no l'anomenin pel seu nom GoF), i el seu hàbitat natural són les canonades de processament configurables, més que no pas l'"exactament un respon" del llibre original.
Quan usar-lo i quan no
Usa'l quan:
- Més d'un objecte pot tractar una petició i l'emissor no ha de saber quin.
- Vols afegir, treure o reordenar passos de processament sense tocar els existents ni l'emissor.
- El conjunt de manipuladors ha de variar per context o configuració (fins i tot en execució).
Evita'l quan:
- Hi ha un sol receptor fix: una crida directa és més simple i més clara. La cadena d'una baula és sobreenginyeria de manual (lliçó 01-06).
- L'emissor necessita garantia de tractament i resposta immediata d'un responsable concret: la cadena introdueix incertesa sobre qui (i si algú) respondrà.
- L'ordre entre passos amaga dependències de dades fortes (cada pas necessita resultats de l'anterior): això és una canonada amb contracte entre etapes, i potser un mètode seqüencial clar és més honest que una pseudocadena rígida.
Cost a pagar: la traça d'execució es fragmenta (per saber quins controls s'apliquen a una comanda cal mirar la configuració, no un mètode), i depurar "per què s'ha rebutjat?" obliga a recórrer baules. Un bon ResultatValidacio amb motiu, i logging a la base amb RegistreEsdeveniments, ho mitiguen molt.
Relació amb altres patrons
Només mencions; cada cosa a la seva lliçó:
- Decorator: mecànica gairebé idèntica (objectes enllaçats que deleguen), intenció oposada — el decorador sempre delega i afegeix; el manipulador decideix si atendre o passar. Acarament complet a la comparativa.
- Command: el que viatja per la cadena pot ser una ordre; GoF els combina sovint (la petició reïficada busca el seu manipulador).
- Composite: en un arbre, el pare de cada node és un "següent" natural — les peticions poden escalar cap a l'arrel.
- Facade: a PideYa, la façana de checkout és el client de la cadena: la dispara sense conèixer-ne les baules.
Errors comuns
- Oblidar delegar al següent en una baula concreta (en variants on cada concret gestiona la crida): els controls posteriors desapareixen sense soroll. Antídot: fontaneria de delegació a la classe base, com hem fet.
- Cadena sense final definit: en la variant "primer competent", no decidir què passa si ningú no atén. Defineix sempre el comportament per defecte (manipulador terminal, valor per defecte o excepció explícita).
- Baules amb estat mutable compartit entre peticions: la mateixa cadena sol servir peticions concurrents; els manipuladors han de ser sense estat (com els nostres: només dependències immutables).
- Ficar el muntatge de la cadena dins de les baules (cadascuna crea la següent amb
new): ressuscita l'acoblament que veníem a matar. El muntatge és de la configuració. - Cadenes quilomètriques per a lògica trivial: quatre
ifseguits que no canviaran mai no necessiten quatre classes. El patró es guanya el pa quan hi ha variabilitat real (ordre, context, reutilització).
Exercicis
Exercici 1: baula nova sense tocar res
El negoci demana un control nou: rebutjar comandes si el restaurant està tancat en la franja horària sol·licitada (comanda.getFranjaLliurament(), restaurant.obertEn(franja)). Escriu GestorHorari i indica on el col·locaries a la cadena i per què.
Exercici 2: cadena d'escalat de suport
Modela amb la variant "primer competent" l'escalat d'incidències: BotSuport resol si la incidència és de tipus PREGUNTA_FREQUENT; AgentSuport resol si quantiaAfectada <= 50; SupervisorSuport resol tota la resta. Dissenya la base GestorIncidencia (en què canvia la seva fontaneria respecte a GestorComanda?) i la baula BotSuport.
Exercici 3: detectar l'antipatró
Un company implementa GestorEstoc així: if (!hiHaEstoc) return rebuig(...); else return new GestorZonaRepartiment(zones).gestionar(comanda);. Assenyala els dos problemes de disseny.
Solucions
Solució 1:
public class GestorHorari extends GestorComanda {
@Override
protected ResultatValidacio comprovar(Comanda comanda) {
return comanda.getRestaurant().obertEn(comanda.getFranjaLliurament())
? ResultatValidacio.ok()
: ResultatValidacio.rebuig("Restaurant tancat en aquesta franja");
}
}Col·locació raonable: al principi (després de frau o fins i tot abans): és una comprovació local i baratíssima que pot estalviar crides cares a estoc i zones. L'important de l'exercici: afegir-lo no toca ni la base, ni les altres baules, ni la façana — només una línia a la configuració. OCP en acció.
Solució 2: la fontaneria canvia en la condició de tall — es talla quan algú atén, no quan algú rebutja; i si ningú no atén, cal decidir el final (aquí, el supervisor és terminal, però la base s'ha de protegir igualment):
public abstract class GestorIncidencia {
private GestorIncidencia seguent;
public GestorIncidencia encadenar(GestorIncidencia s) {
this.seguent = s;
return s;
}
public Resolucio gestionar(Incidencia incidencia) {
if (potResoldre(incidencia)) {
return resoldre(incidencia); // primer competent: aqui acaba
}
if (seguent == null) {
throw new IllegalStateException("Incidencia sense responsable: " + incidencia.getId());
}
return seguent.gestionar(incidencia); // escalar
}
protected abstract boolean potResoldre(Incidencia incidencia);
protected abstract Resolucio resoldre(Incidencia incidencia);
}
public class BotSuport extends GestorIncidencia {
@Override protected boolean potResoldre(Incidencia i) {
return i.getTipus() == TipusIncidencia.PREGUNTA_FREQUENT;
}
@Override protected Resolucio resoldre(Incidencia i) {
return Resolucio.automatica(respostes.cercar(i));
}
}Solució 3: (1) la baula crea amb new la seva següent, cablejant l'ordre dins del manipulador — la cadena deixa de ser configurable i GestorEstoc s'acobla a GestorZonaRepartiment i a les seves dependències; (2) duplica la fontaneria de delegació que ja viu a la base, així que qualsevol canvi de protocol (logging, mètriques) s'haurà de replicar baula a baula. La delegació és de la base; el muntatge, de la configuració.
Conclusió
Chain of Responsibility converteix una seqüència monolítica de controls en baules autònomes: cada GestorComanda fa una sola comprovació, ignora els seus veïns i la cadena completa es decideix en configuració — reordenable, retallable per context i testejable peça a peça. Has vist les seves tres variants (primer competent, veto, canonada), la seva presència ubiqua en filtres i middleware, i el seu preu: la traça fragmentada i la responsabilitat de definir què passa al final de la cadena.
Per la cadena hi ha viatjat la comanda; el patró següent reïfica el viatge mateix. Al panell del restaurant de PideYa, "acceptar comanda", "marcar en preparació" o "cancel·lar" són avui crides que s'executen i s'esfumen: no es poden encuar, ni registrar, ni —el que més demana el restaurant— desfer. Per a això cal convertir cada petició en un objecte amb vida pròpia. Ens veiem a Command.
Curs de Patrons de Disseny de Programari
Mòdul 1: Introducció als Patrons de Disseny
- Què són els Patrons de Disseny?
- Història i Origen dels Patrons de Disseny
- Principis de Disseny: SOLID i Altres Fonaments
- UML Essencial per Entendre Patrons
- Classificació dels Patrons de Disseny
- Avantatges i Desavantatges d'Usar Patrons de Disseny
Mòdul 2: Patrons Creacionals
- Introducció als Patrons Creacionals
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa i Elecció de Patrons Creacionals
Mòdul 3: Patrons Estructurals
- Introducció als Patrons Estructurals
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa i Elecció de Patrons Estructurals
Mòdul 4: Patrons de Comportament
- Introducció als Patrons de Comportament
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa i Elecció de Patrons de Comportament
Mòdul 5: Aplicació de Patrons de Disseny
- Com Seleccionar el Patró Adequat
- Exemples Pràctics d'Ús de Patrons
- Patrons de Disseny en Projectes Reals
- Refactorització Usant Patrons de Disseny
- Antipatrons: Quan els Patrons es Tornen un Problema
Mòdul 6: Patrons de Disseny Avançats
- Patrons de Disseny en Arquitectures Modernes
- Patrons de Disseny en Microserveis
- Patrons de Disseny en Sistemes Distribuïts
- Patrons de Concurrència
- Patrons de Disseny en Desenvolupament Àgil
