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

  1. El problema a PideYa: accions que s'esfumen
  2. Intenció i estructura del patró
  3. Implementació Java completa
  4. Desfer: l'històric d'ordres
  5. Cua d'ordres i macroordres
  6. Command en Java modern: lambdes i Runnable
  7. Quan usar-lo i quan no
  8. Relació amb altres patrons
  9. Errors comuns
  10. 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:

public interface Ordre {
    void executar();
    void desfer();
    String descripcio();
}

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:

  1. 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.
  2. Tot és desfeible? No: una comanda ja lliurada no es "deslliura". Opcions: que desfer() llanci OperacioNoReversibleException, o una interfície a part OrdreReversible extends Ordre i que el panell només apili aquestes.
  3. Històric acotat? Un Deque sense 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 nostre PanellGestio apila 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

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