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

  1. El problema a PideYa: la validació monolítica de la comanda entrant
  2. Intenció i estructura del patró
  3. Implementació Java completa
  4. Muntar la cadena: configurable, no cablejada
  5. Variants: tallar en processar vs. processar i seguir
  6. La cadena a la vida real: filtres i middleware
  7. Quan usar-lo i quan no
  8. Relació amb altres patrons
  9. Errors comuns
  10. 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 if al voltant dels if.
  • 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 omplen comprovar. (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és Estoc. 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 un FilterChain, fa la seva part (autenticació, compressió, CORS...) i decideix si crida chain.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 if seguits 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

Mòdul 2: Patrons Creacionals

Mòdul 3: Patrons Estructurals

Mòdul 4: Patrons de Comportament

Mòdul 5: Aplicació de Patrons de Disseny

Mòdul 6: Patrons de Disseny Avançats

Mòdul 7: Recursos Addicionals i Conclusió

© Copyright 2026. Tots els drets reservats