Editar la cistella a PideYa és un tempteig: el client afegeix dues pizzes, en treu una, aplica un cupó, canvia els extres... i de vegades se'n penedeix i vol tornar a com estava fa tres canvis. A Command vam resoldre el desfer amb operacions inverses; però no tota edició té inversa neta, i calcular-la pot ser més fràgil que el problema. L'alternativa robusta és fotografiar: guardar instantànies de l'estat i restaurar-les. L'obstacle és seriós: per fotografiar la cistella caldria veure'n les tripes — i portem tot el curs defensant l'encapsulació. Memento resol exactament aquesta tensió: capturar i restaurar l'estat d'un objecte sense exposar-ne l'interior a ningú.

Contingut

  1. El problema a PideYa: desfer sense despullar la cistella
  2. Intenció i estructura del patró
  3. Implementació Java completa
  4. Què guardar: l'estat mínim
  5. Segon cas: punts de restauració en editar la carta
  6. Serialització i costos
  7. Variants
  8. Quan usar-lo i quan no
  9. Relació amb altres patrons
  10. Errors comuns
  11. Exercicis i conclusió

El problema a PideYa: desfer sense despullar la cistella

La cistella és una classe amb estat intern curosament protegit:

public class Cistella {

    private final List<LiniaComanda> linies = new ArrayList<>();
    private String codiCupo;                    // pot ser null
    private BigDecimal descompteAplicat = BigDecimal.ZERO;

    public void afegirLinia(Producte producte, int quantitat) { /* valida i afegeix */ }
    public void treureLinia(int posicio) { /* ... */ }
    public void aplicarCupo(String codi) { /* valida contra el servei i fixa descompte */ }
    // getters de nomes lectura per pintar la pantalla; res de setters
}

Volem "desfer" l'edició. Les opcions dolentes:

  • Getters i setters per a tot: getLiniesModificables(), setDescompteAplicat(...)... Qualsevol podria llavors fabricar cistelles incoherents (un descompte sense cupó, línies amb quantitat zero) saltant-se les validacions. L'encapsulació —la garantia que només la cistella toca l'estat de la cistella— mor per servir una sola funcionalitat.
  • Que el codi de la UI es copiï les dades: mateix problema amb un altre nom; a més la UI queda acoblada a cada camp intern — afegeix Cistella un camp nou i el "desfer" restaura cistelles a mitges, silenciosament.
  • Inverses per operació (el desfer() de Command): quina és la inversa exacta d'aplicarCupo si el cupó va recalcular el descompte en funció de les línies d'aleshores? Cal guardar tant context per invertir que ja gairebé és una foto... mal feta.

Les forces en tensió, amb precisió: volem guardar l'estat fora de l'objecte (per restaurar-lo després) però sense que ningú més que l'objecte el pugui llegir o fabricar. Això demana un paquet opac: ple de dades per al seu amo, tancat per a la resta.

Intenció i estructura del patró

Intenció (GoF): sense violar l'encapsulació, capturar i externalitzar l'estat intern d'un objecte, de manera que es pugui restaurar a aquest estat més endavant.

Tres rols amb un repartiment de confiança molt fi:

classDiagram
    class Cistella {
        -linies: List~LiniaComanda~
        -codiCupo: String
        -descompteAplicat: BigDecimal
        +guardarEstat() Memento
        +restaurar(m: Memento)
    }
    class Memento {
        -linies: List~LiniaComanda~
        -codiCupo: String
        -descompteAplicat: BigDecimal
    }
    class HistorialCistella {
        -instantanies: Deque~Memento~
        +guardar(m: Memento)
        +ultima() Memento
    }

    Cistella ..> Memento : crea i llegeix (interficie ampla)
    HistorialCistella o-- Memento : emmagatzema sense mirar (interficie estreta)
Rol GoF A PideYa Què veu del memento
Originator (amo de l'estat) Cistella Tot: el crea i el llegeix (interfície ampla)
Memento (la instantània opaca) Cistella.Memento —
Caretaker (guardià que emmagatzema) HistorialCistella Res: només el guarda i el retorna (interfície estreta)

Aquesta doble interfície és el cor del patró: per a l'originator el memento és transparent; per al caretaker (i el món), una caixa negra amb nansa. El caretaker gestiona quan es guarda i quin es restaura; mai què hi ha a dins.

Implementació Java completa

En Java, la forma natural de la doble interfície és una classe imbricada amb membres privats: Cistella accedeix als camps privats de la seva classe imbricada Memento; ningú més no pot.

public class Cistella {

    private final List<LiniaComanda> linies = new ArrayList<>();
    private String codiCupo;
    private BigDecimal descompteAplicat = BigDecimal.ZERO;

    /** El memento: opac per a tothom llevat de Cistella. */
    public static final class Memento {
        private final List<LiniaComanda> linies;     // privats: el caretaker no els veu
        private final String codiCupo;
        private final BigDecimal descompteAplicat;
        private final Instant moment;                // metadada util per a la UI

        private Memento(Cistella origen) {           // constructor PRIVAT:
            this.linies = List.copyOf(origen.linies); // nomes Cistella fabrica mementos
            this.codiCupo = origen.codiCupo;
            this.descompteAplicat = origen.descompteAplicat;
            this.moment = Instant.now();
        }

        public Instant getMoment() { return moment; } // l'unic public: metadades
    }

    /** Captura: una foto immutable de l'estat actual. */
    public Memento guardarEstat() {
        return new Memento(this);
    }

    /** Restauracio: torna exactament a la foto. */
    public void restaurar(Memento memento) {
        this.linies.clear();
        this.linies.addAll(memento.linies);
        this.codiCupo = memento.codiCupo;
        this.descompteAplicat = memento.descompteAplicat;
    }

    // ... afegirLinia, treureLinia, aplicarCupo com abans ...
}

Tres detalls que fan o trenquen la implementació:

  • List.copyOf(...) a la captura: el memento guarda una còpia immutable, no la referència a la llista viva. Sense això, editar la cistella després mutaria també "la foto" — el bug clàssic del patró. És la mateixa disciplina de còpia profunda que vam aprendre a Prototype: foto que comparteix tripes amb l'original no és foto. (Aquí basta còpia superficial de la llista perquè LiniaComanda és immutable; si no ho fos, còpia profunda.)
  • Constructor privat del memento: ningú no pot fabricar instantànies falses per injectar estat incoherent a la cistella.
  • Metadades públiques (moment): el caretaker pot mostrar a l'usuari "tornar a les 14:32" sense veure el contingut. Interfície estreta no vol dir interfície buida.

El caretaker — fixa't que treballa amb Cistella.Memento sense poder obrir-ne cap:

public class HistorialCistella {

    private static final int MAX_INSTANTANIES = 20;
    private final Deque<Cistella.Memento> instantanies = new ArrayDeque<>();

    public void guardar(Cistella.Memento memento) {
        if (instantanies.size() == MAX_INSTANTANIES) {
            instantanies.removeLast();               // acotat: fora la mes antiga
        }
        instantanies.push(memento);
    }

    public Optional<Cistella.Memento> desfer() {
        return Optional.ofNullable(instantanies.poll());
    }
}

Ús — el guió és: fotografiar abans de cada canvi arriscat; restaurar en penedir-se:

historial.guardar(cistella.guardarEstat());   // foto previa
cistella.aplicarCupo("ESTIU10");              // canvi

historial.guardar(cistella.guardarEstat());
cistella.treureLinia(0);                      // ui, linia equivocada

historial.desfer().ifPresent(cistella::restaurar);  // torna a abans de treure-la
sequenceDiagram
    participant UI as Pantalla cistella
    participant H as HistorialCistella (Caretaker)
    participant C as Cistella (Originator)
    UI->>C: guardarEstat()
    C-->>UI: memento
    UI->>H: guardar(memento)
    UI->>C: treureLinia(0)
    Note over UI: el client prem "desfer"
    UI->>H: desfer()
    H-->>UI: memento
    UI->>C: restaurar(memento)
    Note over H: mai no ha mirat dins del memento

Què guardar: l'estat mínim

La decisió de disseny més important del patró no és al diagrama: què entra a la foto? Regla: l'estat intrínsec i no derivable de l'originator; res més.

Candidat Entra al memento? Per què
Línies de la cistella Sí Estat essencial, origen de tot
Codi de cupó aplicat Sí No es dedueix de les línies
Descompte aplicat Depèn Si sempre es recalcula del cupó + línies, és derivable: millor recalcular en restaurar
Total de la cistella No Derivat pur: es calcula
Referència al ServeiCupons No Dependència, no estat: sobreviu al desfer
Dades del client loguejat No Estat d'un altre objecte; que es fotografiï ell si ho necessita

Guardar de menys → restauracions incoherents. Guardar de més → mementos pesants, fràgils (canvia el servei i les fotos velles ja no encaixen) i fins i tot perillosos (dades de targeta en una instantània?). El memento mínim és més barat i envelleix millor.

Segon cas: punts de restauració en editar la carta

El mateix patró serveix el restaurant: editar la carta (Composite de seccions i plats) és arriscat — reordenar seccions, canviar preus en bloc — i el gestor vol punts de restauració amb nom ("abans d'apujar preus octubre") on tornar dies després. Diferències instructives respecte a la cistella:

  • L'estat és un arbre: la captura exigeix còpia profunda de l'estructura (un altre cop Prototype: un clonar() recursiu del composite és la manera natural de fotografiar-lo).
  • Les instantànies han de sobreviure al reinici → mementos serialitzats a disc/BD (secció següent).
  • El caretaker guanya entitat pròpia: llista de punts amb nom i data, triar quin restaurar — però segueix sense obrir-ne cap.

Serialització i costos

Quan el memento ha de persistir o viatjar, es serialitza (JSON, binari...). Avisos:

  • La serialització és una porta del darrere a l'encapsulació: el JSON del memento exposa els camps que tant vam protegir, i qui el pugui editar pot fabricar estats il·legals. Tracta'l com a frontera de confiança: en restaurar des de fora, revalida (que les quantitats siguin positives, que el cupó encara existeixi...).
  • Versionat: la foto d'octubre s'ha de poder obrir amb el codi de desembre. Afegeix un número de versió al format i un camí de migració, o les instantànies velles es tornen bombes.
  • Cost de memòria/espai: instantània completa × històric llarg × objecte gran = problema. Mitigacions: històric acotat (el nostre MAX_INSTANTANIES), instantànies incrementals (guardar diferències — amb la qual cosa t'acostes a Command), compartir les parts no canviades entre fotos (estructures persistents; si l'estat és immutable, "copiar" és compartir referències, gairebé gratis — un altre dividend de la immutabilitat que venim cultivant des del Builder).

Variants

  • Memento com a classe imbricada privada + interfície marcadora: la versió GoF estricta; el caretaker tipa contra una interfície buida Object-like. La nostra classe imbricada amb constructor privat aconsegueix el mateix amb tipus més útils.
  • Originator = caretaker intern: l'objecte mateix guarda la seva pila d'estats (cistella.desfer()). Menys flexible (política de guardat fixa) però API més simple; raonable quan només hi ha un consumidor de l'undo.
  • Memento incremental: cada foto guarda només el delta respecte a l'anterior. Estalvia espai, complica la restauració (cal reproduir la cadena de deltes).
  • Amb estat immutable: si l'originator és immutable (estil Comanda del Builder), cada versió és el seu propi memento — l'"històric" és una llista de versions i restaurar és apuntar a una de vella. El patró es dissol en l'arquitectura; així funcionen els sistemes de gestió d'estat de les UI modernes (Redux i companyia).

Quan usar-lo i quan no

Usa'l quan:

  • Necessites desfer/restaurar estats i les operacions inverses no existeixen, no són fiables o exigeixen guardar tant context com la foto.
  • Has d'oferir punts de control (checkpoints) o recuperació davant d'errors (capturar abans d'una operació arriscada, restaurar si falla).
  • I, condició irrenunciable: exposar l'estat amb getters/setters trencaria l'encapsulació que protegeix els invariants de l'objecte.

Evita'l quan:

  • L'estat és gran i canvia sovint, i no et pots permetre fotos completes ni la complexitat dels deltes.
  • L'objecte ja és immutable o el seu estat és públic i trivial: guarda referències o còpies sense litúrgia de patró.
  • Només necessites desfer operacions amb inversa òbvia i barata: Command sol és més simple.

Relació amb altres patrons

  • Command: la parella de l'undo robust — l'ordre guarda al seu executar() un memento del receiver, i el seu desfer() el restaura. Inversa quan és fàcil, foto quan no; es combinen sense fricció.
  • Prototype: capturar estat és, mecànicament, clonar; les disciplines de còpia profunda són les mateixes.
  • Iterator: un iterador pot empaquetar la seva posició en un memento per pausar i reprendre recorreguts.
  • State (propera parada): comparteixen la paraula "estat" però no el problema — State canvia el comportament segons l'estat; Memento fotografia les dades. L'aclariment complet, a la comparativa.

Errors comuns

  • Fotos que comparteixen estructures mutables amb l'original (va faltar List.copyOf o la còpia profunda): l'històric sencer apunta al present i el desfer no desfà res. És l'error número u; testeja'l sempre (captura → muta → restaura → verifica).
  • Caretaker que obre el memento (camps package-private "per comoditat", getters afegits "per a un cas"): el patró degenera en un DTO públic i l'encapsulació que venia a protegir desapareix.
  • Històric sense límit: la fuita de memòria de l'undo, agreujada aquí perquè cada entrada és una foto completa.
  • Guardar dependències o estat aliè a la foto: en restaurar, revius referències obsoletes (un servei ja tancat, l'estat d'un altre objecte en versió antiga).
  • Confiar en mementos deserialitzats sense revalidar: estat il·legal injectat per la porta del darrere.

Exercicis

Exercici 1: la foto traïdora

Un company implementa la captura així: this.linies = origen.linies; (sense copiar). Escriu la seqüència mínima de crides (captura, mutació, restauració) que demostra el bug, i explica què observarà l'usuari a la pantalla de la cistella.

Exercici 2: punts de restauració amb nom

Implementa HistorialCarta, caretaker per a l'edició de la carta: crearPunt(String nom, SeccioCarta.Memento m), llistarPunts() (nom + data, per al desplegable de la UI) i recuperar(String nom). Recorda: no pot mirar dins de cap memento. D'on surten el nom i la data que llista?

Exercici 3: inversa o foto?

Per a cada operació del panell del restaurant, decideix si el seu undo convé per operació inversa (Command pur) o per memento, i justifica-ho: (a) marcarEnPreparacio() ↔ tornarAAcceptada(); (b) "reequilibrar preus" (n'apuja uns, n'abaixa d'altres, arrodoneix, segons regles que canvien cada mes); (c) retardarLliurament(minuts).

Solucions

Solució 1:

Cistella.Memento foto = cistella.guardarEstat(); // "foto" (comparteix la llista viva)
cistella.afegirLinia(pizzaCarbonara, 2);         // muta la llista... tambe a la foto
cistella.restaurar(foto);                        // restaura... l'estat ja mutat

La restauració no restaura: la foto apuntava a la mateixa List, així que la carbonara segueix a la cistella. L'usuari prem "desfer" i no passa res de visible — el pitjor tipus de bug: sense excepció, sense log, només un undo que menteix. (Amb el matís d'implementació: restaurar fa clear() + addAll() sobre aquesta mateixa llista compartida; segons l'ordre de les operacions pot fins i tot acabar buidant la "foto". Qualsevol variant del símptoma neix del mateix pecat: no copiar.)

Solució 2:

public class HistorialCarta {

    private record Punt(String nom, SeccioCarta.Memento memento) {}
    private final Map<String, Punt> punts = new LinkedHashMap<>();

    public void crearPunt(String nom, SeccioCarta.Memento memento) {
        punts.put(nom, new Punt(nom, memento));
    }

    public List<String> llistarPunts() {
        return punts.values().stream()
                .map(p -> p.nom() + " (" + p.memento().getMoment() + ")")
                .toList();
    }

    public Optional<SeccioCarta.Memento> recuperar(String nom) {
        return Optional.ofNullable(punts.get(nom)).map(Punt::memento);
    }
}

El nom l'aporta el caretaker (és metadada de gestió, no estat de la carta); la data surt de la metadada pública getMoment() del memento — la interfície estreta permet metadades sense exposar contingut. La restauració en si la farà l'originator: carta.restaurar(memento).

Solució 3: (a) inversa — transició simètrica, barata i estable: Command pur basta; (b) memento — la "inversa" d'un reequilibri amb regles canviants i arrodoniments és pràcticament incomputable (els arrodoniments ni tan sols són bijectius): foto de preus abans, restaurar si no agrada; (c) inversa (retardarLliurament(-minuts)) com vam veure a Command... llevat que el retard dispari recàlculs en cascada, cas en què la foto torna a guanyar. Moralitat: inversa per al que és simètric i barat; memento quan la inversa exigeix reconstruir història.

Conclusió

Memento resol un equilibri que semblava impossible: l'estat de la cistella surt a fora —a l'històric, a disc— sense que ningú llevat de Cistella el pugui llegir ni fabricar, gràcies a la doble interfície (ampla per a l'originator, estreta per al caretaker) i a la classe imbricada amb constructor privat que la materialitza en Java. T'endús també les decisions que separen un undo de joguina d'un de producció: copiar de debò en capturar, fotografiar l'estat mínim no derivable, acotar l'històric, i revalidar tot memento que hagi viatjat. I la regla per triar arma: inversa (Command) quan és simètrica i barata; foto (Memento) quan la inversa menteix o no existeix.

Amb això queden saldats els deutes del desfer. La propera lliçó en salda un de molt més antic — el deute del curs. A la primera lliçó vas veure un Comanda.canviarEstat(...) que creava amb new el push, l'SMS i les estadístiques, i vam prometre que un patró resoldria aquell embolic amb elegància. Tres mòduls després, el moment ha arribat: un canvia, molts se n'assabenten, i ningú no coneix ningú. Ens veiem a Observer.

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