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
- El problema a PideYa: desfer sense despullar la cistella
- Intenció i estructura del patró
- Implementació Java completa
- Què guardar: l'estat mínim
- Segon cas: punts de restauració en editar la carta
- Serialització i costos
- Variants
- Quan usar-lo i quan no
- Relació amb altres patrons
- Errors comuns
- 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
Cistellaun camp nou i el "desfer" restaura cistelles a mitges, silenciosament. - Inverses per operació (el
desfer()de Command): quina és la inversa exacta d'aplicarCuposi 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-lasequenceDiagram
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
Comandadel 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 seudesfer()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.copyOfo 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 mutatLa 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
- 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
