Observer va deixar una pregunta oberta: res no impedeix avui canviarEstat(LLIURADA) sobre una comanda acabada de crear i sense pagar. I el problema és més fondo que validar transicions: gairebé tot el que es pot fer amb una comanda depèn d'on és al seu cicle de vida. Es pot cancel·lar? Gratis si està creada, amb penalització si està en preparació, impossible en repartiment. Es pot modificar? Només abans de pagar. El codi actual respon amb un switch sobre l'estat... repetit a cada mètode, creixent amb cada estat nou. State proposa la reïficació que ja esperes a aquestes altures del mòdul: cada estat és un objecte, amb el comportament d'aquell estat a dins, i la comanda delega en el seu estat actual — que a més decideix a quin es transita.

Contingut

  1. El problema a PideYa: el cicle de vida de la comanda
  2. El primer intent: switch sobre un enum
  3. Intenció i estructura del patró
  4. El diagrama d'estats
  5. Implementació Java completa
  6. Variants de disseny
  7. State vs. switch: la comparació honesta
  8. Quan usar-lo i quan no
  9. Relació amb altres patrons
  10. Errors comuns
  11. Exercicis i conclusió

El problema a PideYa: el cicle de vida de la comanda

Les regles de negoci, tal com les dicta operacions:

Estat Pagar? Modificar línies? Cancel·lar? Transicions sortints
CREADA Sí Sí Sí, gratis PAGADA, CANCELLADA
PAGADA No (ja ho està) No Sí, amb devolució total EN_PREPARACIO, CANCELLADA
EN_PREPARACIO No No Sí, amb penalització del 20% EN_REPARTIMENT, CANCELLADA
EN_REPARTIMENT No No No (ja va de camí) LLIURADA
LLIURADA No No No — (final)
CANCELLADA No No No — (final)

Fixa't en la naturalesa del problema: no és un comportament amb variants (això seria una altra cosa, i té la seva lliçó just després); és el mateix objecte comportant-se com sis objectes diferents segons el moment. La paraula tècnica és que Comanda és una màquina d'estats finits: estats, transicions permeses i comportament per estat.

El primer intent: switch sobre un enum

public class Comanda {

    private EstatComandaEnum estat = EstatComandaEnum.CREADA;

    public void pagar() {
        switch (estat) {
            case CREADA -> { cobrar(); estat = EstatComandaEnum.PAGADA; }
            default -> throw new TransicioInvalidaException("No es pot pagar en " + estat);
        }
    }

    public void cancelar() {
        switch (estat) {
            case CREADA -> estat = EstatComandaEnum.CANCELLADA;
            case PAGADA -> { retornarTot(); estat = EstatComandaEnum.CANCELLADA; }
            case EN_PREPARACIO -> { retornarAmbPenalitzacio(); estat = EstatComandaEnum.CANCELLADA; }
            default -> throw new TransicioInvalidaException("No es pot cancelar en " + estat);
        }
    }

    public void modificarLinia(...) { switch (estat) { /* ... */ } }
    public void marcarEnRepartiment()  { switch (estat) { /* ... */ } }
    // ... un switch per operacio
}

És honest dir-ho: per a sis estats i quatre operacions estables, això funciona i és llegible. Els problemes apareixen amb l'evolució:

  • La lògica de cada estat està escampada per tots els mètodes: per saber "què es pot fer EN_PREPARACIO" cal llegir un case a cada switch de la classe. L'estat no existeix com a concepte al codi; existeix com a etiqueta repetida.
  • Afegir un estat (arriba EN_ESPERA_REPARTIDOR, cortesia de la CentralRepartiment) significa tocar tots els switches — i oblidar-se'n d'un no dona error de compilació si hi ha default.
  • El comportament per estat creix: penalitzacions que depenen del temps en preparació, notificacions diferents per estat... cada case s'engreixa fins que Comanda és un arxivador de condicionals.
  • La combinació estat × operació és propensa al cas oblidat: el bug d'Observer (lliurar sense pagar) era exactament un cas sense default defensiu.

Intenció i estructura del patró

Intenció (GoF): permetre que un objecte alteri el seu comportament quan el seu estat intern canvia. L'objecte semblarà canviar de classe.

"Semblarà canviar de classe" és la frase clau: una Comanda en repartiment es comporta com una altra classe que una de creada — perquè, per sota, el seu comportament viu en una altra classe.

classDiagram
    class Comanda {
        -estat: EstatComanda
        +pagar()
        +cancelar()
        +modificarLinia(...)
        +marcarEnRepartiment()
        ~transitarA(nou: EstatComanda)
    }
    class EstatComanda {
        <<interface>>
        +pagar(c: Comanda)
        +cancelar(c: Comanda)
        +modificarLinia(c: Comanda, ...)
        +marcarEnRepartiment(c: Comanda)
        +nom() String
    }
    class EstatCreada
    class EstatPagada
    class EstatEnPreparacio
    class EstatEnRepartiment
    class EstatLliurada
    class EstatCancelada

    EstatComanda <|.. EstatCreada
    EstatComanda <|.. EstatPagada
    EstatComanda <|.. EstatEnPreparacio
    EstatComanda <|.. EstatEnRepartiment
    EstatComanda <|.. EstatLliurada
    EstatComanda <|.. EstatCancelada
    Comanda o-- EstatComanda : delega en l'actual
Rol GoF A PideYa
Context (manté l'estat actual i delega) Comanda
State (interfície amb les operacions dependents de l'estat) EstatComanda
ConcreteState EstatCreada, EstatPagada, EstatEnPreparacio, EstatEnRepartiment, EstatLliurada, EstatCancelada

El diagrama d'estats

La màquina completa, en el diagrama UML pensat exactament per a això (el vam presentar a la lliçó d'UML; aquí rendeix al màxim):

stateDiagram-v2
    [*] --> CREADA
    CREADA --> PAGADA : pagar()
    CREADA --> CANCELLADA : cancelar() / gratis
    PAGADA --> EN_PREPARACIO : cuina accepta
    PAGADA --> CANCELLADA : cancelar() / devolució total
    EN_PREPARACIO --> EN_REPARTIMENT : repartidor recull
    EN_PREPARACIO --> CANCELLADA : cancelar() / penalització 20%
    EN_REPARTIMENT --> LLIURADA : lliurament confirmat
    LLIURADA --> [*]
    CANCELLADA --> [*]

Consell d'ofici: dibuixa aquest diagrama abans d'escriure una línia del patró. És el contracte amb negoci (de debò no es pot cancel·lar en repartiment?), la llista exacta de classes a escriure, i la font dels tests (cada fletxa, un test que passa; cada no-fletxa, un test que espera excepció).

Implementació Java completa

La interfície State, amb implementacions per defecte que rebutgen: així cada estat concret només escriu el que permet, i el que no s'escriu queda prohibit automàticament — el cas oblidat es torna impossible:

public interface EstatComanda {

    default void pagar(Comanda c)              { rebutjar("pagar", c); }
    default void cancelar(Comanda c)           { rebutjar("cancelar", c); }
    default void modificarLinia(Comanda c, LiniaComanda l) { rebutjar("modificar", c); }
    default void marcarEnPreparacio(Comanda c) { rebutjar("preparar", c); }
    default void marcarEnRepartiment(Comanda c){ rebutjar("repartir", c); }
    default void marcarLliurada(Comanda c)     { rebutjar("lliurar", c); }

    String nom();

    private void rebutjar(String operacio, Comanda c) {
        throw new TransicioInvalidaException(
            "No es pot " + operacio + " una comanda en estat " + nom());
    }
}

Els estats concrets. Cada classe és la columna de la taula de negoci feta codi — compacta, cohesionada, amb nom propi:

public class EstatCreada implements EstatComanda {

    @Override
    public void pagar(Comanda c) {
        c.cobrarTotal();                            // feina del context
        c.transitarA(new EstatPagada());            // transicio decidida AQUI
    }

    @Override
    public void cancelar(Comanda c) {
        c.transitarA(new EstatCancelada());         // gratis: sense devolucio
    }

    @Override
    public void modificarLinia(Comanda c, LiniaComanda linia) {
        c.aplicarModificacio(linia);                // unic estat que ho permet
    }

    @Override
    public String nom() { return "CREADA"; }
}

public class EstatEnPreparacio implements EstatComanda {

    @Override
    public void cancelar(Comanda c) {
        c.retornarAmbPenalitzacio(new BigDecimal("0.20"));
        c.transitarA(new EstatCancelada());
    }

    @Override
    public void marcarEnRepartiment(Comanda c) {
        c.transitarA(new EstatEnRepartiment());
    }

    @Override
    public String nom() { return "EN_PREPARACIO"; }
}

public class EstatLliurada implements EstatComanda {
    // Estat final: no sobreescriu res — tot rebutjat per defecte
    @Override
    public String nom() { return "LLIURADA"; }
}

El context. Comanda queda neta de condicionals: exposa les operacions i delega; i transitarA és l'únic punt per on canvia l'estat — el lloc perfecte per connectar amb Observer:

public class Comanda {

    private EstatComanda estat = new EstatCreada();

    public void pagar()                    { estat.pagar(this); }
    public void cancelar()                 { estat.cancelar(this); }
    public void modificarLinia(LiniaComanda l) { estat.modificarLinia(this, l); }
    public void marcarEnRepartiment()      { estat.marcarEnRepartiment(this); }
    public void marcarLliurada()           { estat.marcarLliurada(this); }

    /** Unica porta de canvi d'estat; visibilitat de paquet: nomes els estats la usen. */
    void transitarA(EstatComanda nou) {
        EstatComanda anterior = this.estat;
        this.estat = nou;
        RegistreEsdeveniments.INSTANCIA.info("Comanda " + id + ": " + anterior.nom() + " → " + nou.nom());
        notificarObservadors(anterior);    // Observer difon el que State autoritza
    }

    // cobrarTotal(), retornarAmbPenalitzacio(...), aplicarModificacio(...): feina real,
    // invocada pels estats; les dades segueixen encapsulades aqui.
}

Les dues peces del mòdul encaixen com estaven destinades: State decideix quines transicions són legals; Observer difon les que ocorren. El bug de "lliurar sense pagar" ja no pot existir — EstatCreada no sobreescriu marcarLliurada, així que la crida mor en una TransicioInvalidaException abans de notificar res.

I el repartiment de responsabilitats importa: els estats decideixen què passa i cap a on es va; el context executa la feina (cobrarTotal) i custodia les seves dades. Estats que graponegen els camps de la comanda són el primer pas cap al fang.

Variants de disseny

  • Estats amb o sense dades pròpies? Els nostres són sense estat propi (tota la dada viu a Comanda): poden ser compartits — constants reutilitzades en comptes de new a cada transició (EstatPagada.INSTANCIA, esperit Flyweight). Si un estat necessita dades pròpies (EN_REPARTIMENT guardant el repartidor assignat), s'instancia per transició i les porta a dins.
  • Qui decideix la transició? Aquí, cada estat (l'habitual: la regla "de CREADA es va a PAGADA" és coneixement de l'estat). Alternativa: el context o una taula de transicions externa decideixen, i els estats només executen comportament — més declaratiu, menys orientat a objectes.
  • Accions d'entrada/sortida: mètodes enEntrar(Comanda)/enSortir(Comanda) a la interfície, invocats per transitarA — lloc idoni per a efectes per estat (en entrar a EN_REPARTIMENT, avisar la CentralRepartiment).
  • State amb enum: en Java, un enum pot implementar mètodes per constant (CREADA { void pagar(...) {...} }, PAGADA {...}) — el patró State complet amb la garantia d'exhaustivitat de l'enum. Excel·lent per a màquines sense dades per estat; es queda curt quan els estats les necessiten (els enum no tenen instàncies per transició).

State vs. switch: la comparació honesta

Criteri switch sobre enum Patró State
Estats i operacions pocs i estables Guanya: menys peces, tot a la vista Burocràcia
Lògica per estat voluminosa/creixent Mètodes-arxivador Guanya: una classe cohesionada per estat
Afegir un estat Tocar N switches (sense ajuda del compilador si hi ha default) Afegir una classe; la resta intacta
Afegir una operació Un mètode nou amb el seu switch Tocar la interfície i (amb defaults) només els estats que la permeten
Veure la màquina completa d'un cop d'ull Regular (escampada per mètodes) Malament (escampada per classes) — mantén l'stateDiagram com a documentació
Dades per estat Incòmode (camps que només valen de vegades) Natural (camps de l'estat concret)

La regla honesta: el switch no és el dolent; és la solució correcta per a màquines petites i estables. State es guanya el pa quan el comportament per estat pesa i la màquina evoluciona — exactament el cas de la comanda de PideYa, el cicle de vida de la qual és el cor canviant del negoci.

Quan usar-lo i quan no

Usa'l quan:

  • El comportament d'un objecte depèn del seu estat i ha de canviar en execució en transitar.
  • Les operacions tenen condicionals grans i repetits sobre el mateix discriminant d'estat.
  • Els estats tenen comportament substancial o dades pròpies, o la màquina creix amb el negoci.

Evita'l quan:

  • La màquina és petita i estable: enum + switch (o enum amb mètodes) és més directe.
  • L'"estat" és només una dada consultada, sense comportament associat: un camp normal basta.
  • Les "variants de comportament" no formen cicle de vida (no hi ha transicions): això és Strategy — la distinció exacta, en una línia aquí i en detall a la comparativa: State transita sol entre estats que es coneixen; Strategy és triada des de fora i ignora les seves germanes.

Relació amb altres patrons

  • Strategy: estructuralment bessons (context que delega en un objecte intercanviable); la diferència és d'intenció i de qui canvia l'objecte — menció feta, acarament a la lliçó següent i a la comparativa.
  • Observer: State autoritza transicions, Observer les difon; connectats a transitarA.
  • Singleton/Flyweight: estats sense dades compartits com a instàncies úniques.
  • Memento: per fer retrocedir la màquina a un punt anterior, es fotografia el context (inclòs quin estat era).
  • Command: les ordres del panell (OrdreCancelarComanda) invoquen operacions la legalitat de les quals decideix ara State — el desfer() ha de comptar amb la TransicioInvalidaException.

Errors comuns

  • Transicions des de fora: un comanda.setEstat(new EstatLliurada()) públic destrueix la màquina — qualsevol se salta les regles. La porta (transitarA) ha de ser inaccessible a l'exterior (paquet/privada).
  • Estats que toquen les dades del context directament: els estats decideixen, el context executa; si EstatPagada manipula camps de Comanda, l'encapsulació es trenca i els estats s'acoblen a les tripes.
  • Oblidar el comportament per defecte: sense els default que rebutgen, cada estat ha d'implementar-ho tot, i el cas no contemplat torna com a null o silenci en comptes d'excepció clara.
  • Explosió d'estats per combinar dimensions: si la comanda necessita "estat de pagament" × "estat de cuina" independents, no creïs PagadaIAlForn: són dues màquines paral·leles, cadascuna amb el seu State.
  • Duplicar el discriminant: mantenir a més un enum estat que cal sincronitzar a mà amb l'objecte estat — dues fonts de veritat. Si necessites el nom per persistir o mostrar, deriva'l de l'objecte (estat.nom()), no el guardis a part.

Exercicis

Exercici 1: estat nou sense tocar els vells

Operacions introdueix EN_ESPERA_REPARTIDOR: després d'EN_PREPARACIO, si no hi ha repartidor, la comanda espera; quan la CentralRepartiment n'assigna un, passa a EN_REPARTIMENT; es pot cancel·lar amb devolució total (el retard és culpa nostra). Implementa EstatEnEsperaRepartidor i indica quines classes existents canvien.

Exercici 2: dibuixar abans de programar

Modela amb un stateDiagram-v2 de mermaid la màquina d'estats d'una incidència de suport: OBERTA → EN_CURS (quan un agent la pren) → RESOLTA (amb solució) → TANCADA (el client confirma); des de RESOLTA pot tornar a EN_CURS (el client reclama); des d'OBERTA i EN_CURS pot passar a TANCADA directament si el client la retira.

Exercici 3: State, switch o Strategy?

Justifica l'eina per a cada cas: (a) el semàfor de disponibilitat d'un repartidor: DISPONIBLE ↔ OCUPAT ↔ DESCANS, sense comportament associat més que el valor mateix; (b) el document d'alta d'un restaurant: ESBORRANY / EN_REVISIO / PUBLICAT / RETIRAT, amb permisos d'edició, validacions i notificacions diferents per estat, i estats nous previstos; (c) tres maneres de calcular el repartiment de la propina entre repartidors, elegible per país.

Solucions

Solució 1:

public class EstatEnEsperaRepartidor implements EstatComanda {

    @Override
    public void cancelar(Comanda c) {
        c.retornarTot();                        // culpa nostra: sense penalitzacio
        c.transitarA(new EstatCancelada());
    }

    @Override
    public void marcarEnRepartiment(Comanda c) { // ho invoca l'assignacio de la central
        c.transitarA(new EstatEnRepartiment());
    }

    @Override
    public String nom() { return "EN_ESPERA_REPARTIDOR"; }
}

Canvia només EstatEnPreparacio: el seu marcarEnRepartiment decideix ara entre transitar a EnRepartiment (hi ha repartidor) o a EnEsperaRepartidor (no n'hi ha) — és l'estat origen de la fletxa nova, i les fletxes són coneixement del seu origen. Comanda, la interfície i els altres quatre estats queden intactes. Compara amb l'enfocament switch: haurien canviat tots els mètodes amb switch.

Solució 2:

stateDiagram-v2
    [*] --> OBERTA
    OBERTA --> EN_CURS : agent la pren
    OBERTA --> TANCADA : client la retira
    EN_CURS --> RESOLTA : solució proposada
    EN_CURS --> TANCADA : client la retira
    RESOLTA --> EN_CURS : client reclama
    RESOLTA --> TANCADA : client confirma
    TANCADA --> [*]

(El valor de l'exercici és en les preguntes que el diagrama obliga a fer: es pot reobrir una TANCADA? Segons l'enunciat, no — i ara està escrit.)

Solució 3: (a) enum i prou (ni tan sols switch): tres valors sense comportament — aplicar State seria la sobreenginyeria de la lliçó 01-06; (b) State: comportament substancial per estat, transicions amb regles i màquina en creixement — el cas de manual; (c) Strategy: són algorismes alternatius triats per configuració (país), sense transicions entre ells — cap càlcul de propina no "transita" a un altre. Que és, precisament, la lliçó següent.

Conclusió

State reïfica el cicle de vida: cada etapa de la comanda és ara una classe que concentra el seu comportament i coneix les seves sortides, Comanda delega sense un sol condicional, i les transicions il·legals moren a la porta amb nom i cognoms — el forat que Observer va deixar obert queda tancat, i tots dos patrons col·laboren a transitarA: State autoritza, Observer difon. T'endús també el criteri honest (switch per a màquines petites i estables; State quan el comportament pesa i la màquina evoluciona) i l'hàbit d'or: primer el diagrama d'estats, després el codi.

A l'acarament final de l'exercici ha tret el cap la frontera exacta amb el patró següent: càlculs alternatius que no transiten, triats des de fora. PideYa en té dos de llibre esperant: les despeses d'enviament (per distància, tarifa plana, gratis per promoció) i l'assignació de repartidor que la CentralRepartiment va deixar pendent d'extreure (més proper, menys carregat, round-robin). Famílies d'algorismes intercanviables rere una interfície: ens veiem a Strategy.

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