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
- El problema a PideYa: el cicle de vida de la comanda
- El primer intent: switch sobre un enum
- Intenció i estructura del patró
- El diagrama d'estats
- Implementació Java completa
- Variants de disseny
- State vs. switch: la comparació honesta
- Quan usar-lo i quan no
- Relació amb altres patrons
- Errors comuns
- 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
casea cadaswitchde 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 hadefault. - El comportament per estat creix: penalitzacions que depenen del temps en preparació, notificacions diferents per estat... cada
cases'engreixa fins queComandaé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
defaultdefensiu.
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 denewa 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 pertransitarA— 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 — eldesfer()ha de comptar amb laTransicioInvalidaException.
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
EstatPagadamanipula camps deComanda, l'encapsulació es trenca i els estats s'acoblen a les tripes. - Oblidar el comportament per defecte: sense els
defaultque rebutgen, cada estat ha d'implementar-ho tot, i el cas no contemplat torna com anullo 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
estatque 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
- 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
