El panell de gestió del restaurant a PideYa és una botonera: acceptar comanda, marcar en preparació, avisar de retard, cancel·lar. Cada botó avui crida directament un mètode, la crida s'executa i desapareix. Però el restaurant demana coses que una crida efímera no pot donar: desfer l'última acció (el dit rellisca i cancel·la la comanda equivocada), una cua d'operacions quan la cuina va saturada, un registre de qui va fer què, i dreceres que executen diverses accions de cop. Command ho resol tot amb un moviment: convertir cada petició en un objecte — i un cop la petició és un objecte, es pot desar, encuar, revertir i compondre.
Contingut
- El problema a PideYa: accions que s'esfumen
- Intenció i estructura del patró
- Implementació Java completa
- Desfer: l'històric d'ordres
- Cua d'ordres i macroordres
- Command en Java modern: lambdes i
Runnable - Quan usar-lo i quan no
- Relació amb altres patrons
- Errors comuns
- Exercicis i conclusió
El problema a PideYa: accions que s'esfumen
La versió actual del panell acobla cada botó a la lògica que executa:
public class PanellGestioUI {
public void onClickAcceptar(Comanda comanda) {
comanda.acceptar();
notificador.enviar("La teva comanda ha estat acceptada");
}
public void onClickCancelar(Comanda comanda) {
comanda.cancelar();
passarella.retornarCobrament(comanda);
notificador.enviar("La teva comanda ha estat cancellada");
}
// ... un metode per boto, sense memoria del que s'ha fet
}El que aquest disseny no pot fer:
- Desfer: executada la cancel·lació, no queda rastre de què es va fer ni de com revertir-ho.
- Encuar: a l'hora punta el restaurant vol acceptar comandes i que es processin per ordre; una crida directa s'executa ara o no s'executa.
- Registrar/auditar: "qui va cancel·lar la comanda 4412 i quan?" exigeix escampar logging per cada mètode.
- Reutilitzar l'acció des d'un altre lloc (API, drecera de teclat, automatització "acceptar tot el de menys de 20 €"): la lògica està soldada al botó.
- Compondre: la drecera "tancar cuina" (rebutjar pendents + avisar repartiment + pausar la carta) no té on viure.
El denominador comú: la petició existeix només com a crida a la pila. El que necessitem és cosificar-la — que "cancel·lar la comanda 4412" sigui una dada amb què treballar.
Intenció i estructura del patró
Intenció (GoF): encapsular una petició com un objecte, permetent parametritzar clients amb peticions diferents, encuar o registrar peticions, i suportar operacions desfeibles.
Quatre rols, i la clau és qui ignora qui: l'Invoker (la botonera) executa ordres sense saber què fan; el Receiver (la comanda, la passarel·la) fa la feina sense saber qui l'ha demanada; el Command els uneix guardant tot el necessari per executar (i revertir) la petició.
classDiagram
class Ordre {
<<interface>>
+executar()
+desfer()
+descripcio() String
}
class OrdreAcceptarComanda {
-comanda: Comanda
-notificador: Notificador
+executar()
+desfer()
}
class OrdreCancelarComanda {
-comanda: Comanda
-passarella: PassarellaPagament
-notificador: Notificador
+executar()
+desfer()
}
class PanellGestio {
-historic: Deque~Ordre~
+executar(o: Ordre)
+desferUltima()
}
class Comanda {
+acceptar()
+cancelar()
+reobrir()
}
Ordre <|.. OrdreAcceptarComanda
Ordre <|.. OrdreCancelarComanda
PanellGestio o-- Ordre : historic
OrdreAcceptarComanda --> Comanda : receiver
OrdreCancelarComanda --> Comanda : receiver
| Rol GoF | A PideYa |
|---|---|
| Command (interfície de la petició) | Ordre |
| ConcreteCommand | OrdreAcceptarComanda, OrdreCancelarComanda, OrdreMarcarEnPreparacio... |
| Invoker (dispara ordres, guarda l'històric) | PanellGestio |
| Receiver (qui fa la feina real) | Comanda, PassarellaPagament, Notificador |
| Client (crea l'ordre i en fixa el receiver) | el codi de la UI / configuració |
sequenceDiagram
participant UI as Boto cancelar
participant P as PanellGestio (Invoker)
participant C as OrdreCancelarComanda
participant R as Comanda (Receiver)
UI->>C: new OrdreCancelarComanda(comanda, ...)
UI->>P: executar(ordre)
P->>C: executar()
C->>R: cancelar()
P->>P: historic.push(ordre)
Note over P: mes tard...
UI->>P: desferUltima()
P->>C: desfer()
C->>R: reobrir()
Implementació Java completa
La interfície Command. Petita a propòsit; descripcio() alimenta l'auditoria i el rètol "Desfés cancel·lar comanda 4412" de la UI:
Una ordre concreta. Fixa-t'hi: rep al constructor tot el que necessita (receiver inclòs) — l'ordre és una crida congelada, llesta per disparar en qualsevol moment i lloc:
public class OrdreCancelarComanda implements Ordre {
private final Comanda comanda;
private final PassarellaPagament passarella;
private final Notificador notificadorClient;
public OrdreCancelarComanda(Comanda comanda, PassarellaPagament passarella,
Notificador notificadorClient) {
this.comanda = comanda;
this.passarella = passarella;
this.notificadorClient = notificadorClient;
}
@Override
public void executar() {
comanda.cancelar();
passarella.retornarCobrament(comanda.getId(), comanda.getTotal());
notificadorClient.enviar("La teva comanda " + comanda.getId() + " ha estat cancellada");
}
@Override
public void desfer() {
comanda.reobrir();
passarella.cobrar(comanda.getId(), comanda.getTotal());
notificadorClient.enviar("Cancellacio anullada: la teva comanda " + comanda.getId() + " segueix en marxa");
}
@Override
public String descripcio() {
return "Cancelar comanda " + comanda.getId();
}
}OrdreAcceptarComanda i OrdreMarcarEnPreparacio segueixen el motlle: executar() avança (comanda.acceptar(), comanda.marcarEnPreparacio()) i desfer() retrocedeix (comanda.tornarAPendent(), comanda.tornarAAcceptada()). Quines transicions són legals no és assumpte de l'ordre: això ho governarà State dins de Comanda.
L'Invoker. No sap què fa cap ordre; sap executar-les, recordar-les i auditar-les:
public class PanellGestio {
private final Deque<Ordre> historic = new ArrayDeque<>();
public void executar(Ordre ordre) {
ordre.executar();
historic.push(ordre);
RegistreEsdeveniments.INSTANCIA.info("Executada: " + ordre.descripcio());
}
public void desferUltima() {
if (historic.isEmpty()) {
return;
}
Ordre ultima = historic.pop();
ultima.desfer();
RegistreEsdeveniments.INSTANCIA.info("Desfeta: " + ultima.descripcio());
}
}El client munta i dispara:
panell.executar(new OrdreCancelarComanda(comanda, passarella, notificadorClient));
// ui, comanda equivocada!
panell.desferUltima();La botonera, l'API pública i l'automatització "acceptar comandes petites" ara executen els mateixos objectes ordre: l'acció viu en un sol lloc.
Desfer: l'històric d'ordres
El Deque com a pila és l'essència de l'undo: LIFO — es desfà l'últim primer. Tres decisions de disseny que tot undo real ha de prendre:
- Desfer per inversa o per instantània? El nostre
desfer()executa l'operació inversa (reobrir, tornar a cobrar). Funciona quan la inversa existeix i és fiable. Quan no n'hi ha (quina és la inversa de "batre els ous"?), l'alternativa és guardar una foto de l'estat previ i restaurar-la — això és exactament Memento, parella clàssica de Command, i allà ho desenvolupem. - Tot és desfeible? No: una comanda ja lliurada no es "deslliura". Opcions: que
desfer()llanciOperacioNoReversibleException, o una interfície a partOrdreReversible extends Ordrei que el panell només apili aquestes. - Històric acotat? Un
Dequesense límit és una fuita de memòria en un panell que corre setmanes. Acota'l (p. ex., 50 entrades) descartant pel fons.
Per refer (redo), afegeix una segona pila: desferUltima() mou l'ordre a pilaRefer; executar una ordre nova la buida.
Cua d'ordres i macroordres
Cua. Com que l'ordre és una dada, executar "ara" o "quan toqui" és només canviar l'invoker: una BlockingQueue<Ordre> on la botonera diposita i un fil treballador drena. La cuina saturada processa per ordre d'arribada, sense perdre res, i el productor no espera. (Aquesta ordre-que-viatja és el germen dels busos de missatges i cues de tasques que veurem al mòdul 6.)
Macroordre. Una ordre els fills de la qual són ordres — la idea de Composite aplicada a les accions:
public class MacroOrdre implements Ordre {
private final List<Ordre> passos;
public MacroOrdre(String nom, List<Ordre> passos) { /* ... */ }
@Override
public void executar() {
passos.forEach(Ordre::executar);
}
@Override
public void desfer() {
// en ordre invers! L'ultim fet es el primer desfet
for (int i = passos.size() - 1; i >= 0; i--) {
passos.get(i).desfer();
}
}
}La drecera "tancar cuina" és una MacroOrdre amb tres passos — i el panell l'executa, apila i desfà exactament igual que una ordre simple. Detall amb molla: si el pas 2 de 3 falla a mig executar(), desfàs els ja executats? Això és una compensació, i és la llavor del patró Saga de microserveis (mòdul 6).
Command en Java modern: lambdes i Runnable
Runnable és, literalment, la interfície Command amb un sol mètode i sense undo: ExecutorService.submit(Runnable) és un invoker amb cua d'ordres de sèrie al JDK. I si la teva ordre no necessita desfer() ni descripcio(), una interfície funcional i lambdes basten:
@FunctionalInterface
public interface Accio { void executar(); }
Map<String, Accio> dreceres = Map.of(
"acceptar", () -> comanda.acceptar(),
"preparant", () -> comanda.marcarEnPreparacio()
);
dreceres.get("acceptar").executar();És el mateix esperit del RegistreNotificadors amb Suppliers del Factory Method: la lambda captura receiver i arguments, com el constructor de l'ordre. La regla pràctica: lambda per a "executar i oblidar"; classe quan l'ordre té estat o contracte (undo, descripció, auditoria, serialització). El nostre panell necessita les tres coses: classes.
Quan usar-lo i quan no
Usa'l quan:
- Necessites desfer/refer, històric o auditoria d'operacions.
- Vols encuar, endarrerir o programar peticions (executar en un altre moment, un altre fil, una altra màquina).
- Vols parametritzar objectes amb accions (botons, menús, dreceres configurables) o compondre operacions (macros).
- Necessites desacoblar qui demana de qui fa i de quan es fa.
Evita'l quan:
- És una crida directa, síncrona, sense històric ni variabilitat:
comanda.acceptar()sense més és més clar que tres classes al voltant. - La "reversibilitat" és il·lusòria (efectes externs irreversibles: emails enviats, cobraments liquidats): un undo que no desfà de debò és pitjor que no tenir-lo.
Cost: una classe per operació (mitigable amb lambdes per als casos simples) i la disciplina de mantenir desfer() com a mirall fidel d'executar() — cada canvi en una s'ha de reflectir en l'altra.
Relació amb altres patrons
- Memento: l'undo per instantània; Command+Memento és el duo clàssic del desfer robust.
- Composite: la
MacroOrdreés un composite d'ordres. - Chain of Responsibility: els manipuladors de la cadena poden rebre ordres com a petició.
- Strategy: semblança superficial (objecte que encapsula codi); intenció diferent — Strategy encapsula com es fa una cosa; Command, que es va demanar una cosa. Acarament a la comparativa.
- Prototype: ordres que es clonen com a plantilla abans d'ajustar paràmetres.
Errors comuns
- Ordres que fan la feina elles mateixes en comptes de delegar en receivers: l'ordre s'engreixa fins a ser un "mètode disfressat de classe" impossible de reutilitzar. L'ordre coordina; el receiver fa.
desfer()dessincronitzat d'executar(): s'afegeix un efecte a l'execució (una notificació, un cobrament) i ningú no toca la inversa. Testeja'ls en parella:executar(); desfer();ha de deixar el sistema com estava.- Històric sense límit: fuita de memòria silenciosa (i amb Memento a dins, greu).
- Apilar ordres fallides: si
executar()llança excepció, no ha d'entrar a l'històric (fixa't que el nostrePanellGestioapila després d'executar, precisament per això). - Desfer macros en el mateix ordre que es van executar: les inverses s'han d'aplicar en ordre invers, o les dependències entre passos es trenquen.
Exercicis
Exercici 1: ordre de retard
Implementa OrdreAvisarRetard que suma minuts a l'hora estimada de la comanda (comanda.retardarLliurament(minuts)) i notifica el client. El seu desfer() ha de restar el retard i avisar que l'hora torna a ser l'original. Què guarda l'ordre per poder desfer?
Exercici 2: refer
Amplia PanellGestio amb referUltima() usant una segona pila. Regla: executar una ordre nova invalida el que es pot refer. Escriu la classe completa.
Exercici 3: ordre o lambda?
Per a cada cas, decideix si usaries una classe ordre completa o una lambda/Runnable, i per què: (a) refrescar la llista de comandes en prémer F5; (b) cancel·lar comanda amb devolució de cobrament; (c) els passos interns de la macro "tancar cuina"; (d) reintentar l'enviament d'una notificació en segon pla.
Solucions
Solució 1: l'ordre guarda els minuts que va aplicar (el seu paràmetre és alhora la informació d'undo, perquè la inversa és "restar el que s'ha sumat"):
public class OrdreAvisarRetard implements Ordre {
private final Comanda comanda;
private final Notificador notificadorClient;
private final int minuts;
public OrdreAvisarRetard(Comanda comanda, Notificador n, int minuts) {
this.comanda = comanda;
this.notificadorClient = n;
this.minuts = minuts;
}
@Override public void executar() {
comanda.retardarLliurament(minuts);
notificadorClient.enviar("La teva comanda es retarda " + minuts + " min. Disculpa!");
}
@Override public void desfer() {
comanda.retardarLliurament(-minuts);
notificadorClient.enviar("Bones noticies! La teva comanda torna a l'hora prevista");
}
@Override public String descripcio() {
return "Retardar comanda " + comanda.getId() + " (" + minuts + " min)";
}
}Si la inversa no fos un simple -minuts (p. ex., si retardar recalcula rutes), caldria guardar l'hora estimada prèvia — i tan bon punt l'estat a guardar creix, estàs demanant Memento.
Solució 2:
public class PanellGestio {
private final Deque<Ordre> pilaDesfer = new ArrayDeque<>();
private final Deque<Ordre> pilaRefer = new ArrayDeque<>();
public void executar(Ordre ordre) {
ordre.executar();
pilaDesfer.push(ordre);
pilaRefer.clear(); // el nou invalida el que es pot refer
}
public void desferUltima() {
if (pilaDesfer.isEmpty()) return;
Ordre o = pilaDesfer.pop();
o.desfer();
pilaRefer.push(o);
}
public void referUltima() {
if (pilaRefer.isEmpty()) return;
Ordre o = pilaRefer.pop();
o.executar();
pilaDesfer.push(o);
}
}Solució 3: (a) lambda — sense estat, sense undo, pura acció d'UI; (b) classe — necessita undo, descripció per a l'auditoria i coordina diversos receivers; (c) classes — la macro necessita poder desfer-los en ordre invers, així que han de complir el contracte complet d'Ordre; (d) Runnable en un executor — és "executar i oblidar" diferit; el reintent ja l'aporta el NotificadorAmbReintents del mòdul 3, no l'ordre.
Conclusió
Command reïfica la petició: "cancel·lar la comanda 4412" ha deixat de ser una crida fugaç per ser un objecte amb executar(), desfer() i descripcio(), que el PanellGestio dispara, apila, audita i reverteix sense saber què hi ha a dins. De regal: cues d'ordres per a l'hora punta, macroordres per a les dreceres compostes, i la versió lleugera amb lambdes per a les accions sense contracte. I ha quedat plantada la llavor de Memento: quan la inversa no basta, es guarda una foto.
El proper patró també converteix una cosa en objectes, però una cosa més exòtica: frases d'un llenguatge. Màrqueting vol escriure regles de promoció com "total > 30 I dia == DIVENDRES → 10% de descompte" sense esperar un desplegament. Per avaluar-les necessitem una gramàtica, un arbre d'expressions i un intèrpret — i també necessitem parlar amb franquesa de per què aquest patró és el menys usat del catàleg. Ens veiem a Interpreter.
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
