Editar el carrito en PideYa es un tanteo: el cliente añade dos pizzas, quita una, aplica un cupón, cambia los extras... y a veces se arrepiente y quiere volver a como estaba hace tres cambios. En Command resolvimos el deshacer con operaciones inversas; pero no toda edición tiene inversa limpia, y calcularla puede ser más frágil que el problema. La alternativa robusta es fotografiar: guardar instantáneas del estado y restaurarlas. El obstáculo es serio: para fotografiar el carrito habría que ver sus tripas — y llevamos todo el curso defendiendo la encapsulación. Memento resuelve exactamente esa tensión: capturar y restaurar el estado de un objeto sin exponer su interior a nadie.
Contenido
- El problema en PideYa: deshacer sin desnudar el carrito
- Intención y estructura del patrón
- Implementación Java completa
- Qué guardar: el estado mínimo
- Segundo caso: puntos de restauración al editar la carta
- Serialización y costes
- Variantes
- Cuándo usarlo y cuándo no
- Relación con otros patrones
- Errores comunes
- Ejercicios y conclusión
El problema en PideYa: deshacer sin desnudar el carrito
El carrito es una clase con estado interno cuidadosamente protegido:
public class Carrito {
private final List<LineaPedido> lineas = new ArrayList<>();
private String codigoCupon; // puede ser null
private BigDecimal descuentoAplicado = BigDecimal.ZERO;
public void añadirLinea(Producto producto, int cantidad) { /* valida y añade */ }
public void quitarLinea(int posicion) { /* ... */ }
public void aplicarCupon(String codigo) { /* valida contra el servicio y fija descuento */ }
// getters de solo lectura para pintar la pantalla; nada de setters
}Queremos "deshacer" la edición. Las opciones malas:
- Getters y setters para todo:
getLineasModificables(),setDescuentoAplicado(...)... Cualquiera podría entonces fabricar carritos incoherentes (un descuento sin cupón, líneas con cantidad cero) saltándose las validaciones. La encapsulación —la garantía de que solo el carrito toca el estado del carrito— muere para servir a una sola funcionalidad. - Que el código de la UI se copie los datos: mismo problema con otro nombre; además la UI queda acoplada a cada campo interno — añade
Carritoun campo nuevo y el "deshacer" restaura carritos a medias, silenciosamente. - Inversas por operación (el
deshacer()de Command): ¿cuál es la inversa exacta deaplicarCuponsi el cupón recalculó el descuento en función de las líneas de entonces? Hay que guardar tanto contexto para invertir que ya casi es una foto... mal hecha.
Las fuerzas en tensión, con precisión: queremos guardar el estado fuera del objeto (para restaurarlo después) pero sin que nadie más que el objeto pueda leerlo o fabricarlo. Eso pide un paquete opaco: lleno de datos para su dueño, cerrado para el resto.
Intención y estructura del patrón
Intención (GoF): sin violar la encapsulación, capturar y externalizar el estado interno de un objeto, de modo que pueda restaurarse a ese estado más adelante.
Tres roles con un reparto de confianza muy fino:
classDiagram
class Carrito {
-lineas: List~LineaPedido~
-codigoCupon: String
-descuentoAplicado: BigDecimal
+guardarEstado() Memento
+restaurar(m: Memento)
}
class Memento {
-lineas: List~LineaPedido~
-codigoCupon: String
-descuentoAplicado: BigDecimal
}
class HistorialCarrito {
-instantaneas: Deque~Memento~
+guardar(m: Memento)
+ultima() Memento
}
Carrito ..> Memento : crea y lee (interfaz amplia)
HistorialCarrito o-- Memento : almacena sin mirar (interfaz estrecha)
| Rol GoF | En PideYa | Qué ve del memento |
|---|---|---|
| Originator (dueño del estado) | Carrito |
Todo: lo crea y lo lee (interfaz amplia) |
| Memento (la instantánea opaca) | Carrito.Memento |
— |
| Caretaker (guardián que almacena) | HistorialCarrito |
Nada: solo lo guarda y lo devuelve (interfaz estrecha) |
Esa doble interfaz es el corazón del patrón: para el originator el memento es transparente; para el caretaker (y el mundo), una caja negra con asa. El caretaker gestiona cuándo se guarda y cuál se restaura; jamás qué hay dentro.
Implementación Java completa
En Java, la forma natural de la doble interfaz es una clase anidada con miembros privados: Carrito accede a los campos privados de su clase anidada Memento; nadie más puede.
public class Carrito {
private final List<LineaPedido> lineas = new ArrayList<>();
private String codigoCupon;
private BigDecimal descuentoAplicado = BigDecimal.ZERO;
/** El memento: opaco para todos salvo para Carrito. */
public static final class Memento {
private final List<LineaPedido> lineas; // privados: el caretaker no los ve
private final String codigoCupon;
private final BigDecimal descuentoAplicado;
private final Instant momento; // metadato útil para la UI
private Memento(Carrito origen) { // constructor PRIVADO:
this.lineas = List.copyOf(origen.lineas); // solo Carrito fabrica mementos
this.codigoCupon = origen.codigoCupon;
this.descuentoAplicado = origen.descuentoAplicado;
this.momento = Instant.now();
}
public Instant getMomento() { return momento; } // lo único público: metadatos
}
/** Captura: una foto inmutable del estado actual. */
public Memento guardarEstado() {
return new Memento(this);
}
/** Restauración: vuelve exactamente a la foto. */
public void restaurar(Memento memento) {
this.lineas.clear();
this.lineas.addAll(memento.lineas);
this.codigoCupon = memento.codigoCupon;
this.descuentoAplicado = memento.descuentoAplicado;
}
// ... añadirLinea, quitarLinea, aplicarCupon como antes ...
}Tres detalles que hacen o rompen la implementación:
List.copyOf(...)en la captura: el memento guarda una copia inmutable, no la referencia a la lista viva. Sin esto, editar el carrito después mutaría también "la foto" — el bug clásico del patrón. Es la misma disciplina de copia profunda que aprendimos en Prototype: foto que comparte tripas con el original no es foto. (Aquí basta copia superficial de la lista porqueLineaPedidoes inmutable; si no lo fuera, copia profunda.)- Constructor privado del memento: nadie puede fabricar instantáneas falsas para inyectar estado incoherente al carrito.
- Metadatos públicos (
momento): el caretaker puede mostrarle al usuario "volver a las 14:32" sin ver el contenido. Interfaz estrecha no significa interfaz vacía.
El caretaker — fíjate en que trabaja con Carrito.Memento sin poder abrir ninguno:
public class HistorialCarrito {
private static final int MAX_INSTANTANEAS = 20;
private final Deque<Carrito.Memento> instantaneas = new ArrayDeque<>();
public void guardar(Carrito.Memento memento) {
if (instantaneas.size() == MAX_INSTANTANEAS) {
instantaneas.removeLast(); // acotado: fuera la más antigua
}
instantaneas.push(memento);
}
public Optional<Carrito.Memento> deshacer() {
return Optional.ofNullable(instantaneas.poll());
}
}Uso — el guion es: fotografiar antes de cada cambio arriesgado; restaurar al arrepentirse:
historial.guardar(carrito.guardarEstado()); // foto previa
carrito.aplicarCupon("VERANO10"); // cambio
historial.guardar(carrito.guardarEstado());
carrito.quitarLinea(0); // ups, línea equivocada
historial.deshacer().ifPresent(carrito::restaurar); // vuelve a antes de quitarlasequenceDiagram
participant UI as Pantalla carrito
participant H as HistorialCarrito (Caretaker)
participant C as Carrito (Originator)
UI->>C: guardarEstado()
C-->>UI: memento
UI->>H: guardar(memento)
UI->>C: quitarLinea(0)
Note over UI: el cliente pulsa "deshacer"
UI->>H: deshacer()
H-->>UI: memento
UI->>C: restaurar(memento)
Note over H: nunca miró dentro del memento
Qué guardar: el estado mínimo
La decisión de diseño más importante del patrón no está en el diagrama: ¿qué entra en la foto? Regla: el estado intrínseco y no derivable del originator; nada más.
| Candidato | ¿Entra en el memento? | Por qué |
|---|---|---|
| Líneas del carrito | Sí | Estado esencial, origen de todo |
| Código de cupón aplicado | Sí | No se deduce de las líneas |
| Descuento aplicado | Depende | Si siempre se recalcula del cupón + líneas, es derivable: mejor recalcular al restaurar |
| Total del carrito | No | Derivado puro: se calcula |
Referencia al ServicioCupones |
No | Dependencia, no estado: sobrevive al deshacer |
| Datos del cliente logueado | No | Estado de otro objeto; que se fotografíe él si lo necesita |
Guardar de menos → restauraciones incoherentes. Guardar de más → mementos pesados, frágiles (cambia el servicio y las fotos viejas ya no encajan) y hasta peligrosos (¿datos de tarjeta en una instantánea?). El memento mínimo es más barato y envejece mejor.
Segundo caso: puntos de restauración al editar la carta
El mismo patrón sirve al restaurante: editar la carta (Composite de secciones y platos) es arriesgado — reordenar secciones, cambiar precios en bloque — y el gestor quiere puntos de restauración con nombre ("antes de subir precios octubre") a los que volver días después. Diferencias instructivas respecto al carrito:
- El estado es un árbol: la captura exige copia profunda de la estructura (otra vez Prototype: un
clonar()recursivo del composite es la forma natural de fotografiarlo). - Las instantáneas deben sobrevivir al reinicio → mementos serializados a disco/BD (siguiente sección).
- El caretaker gana entidad propia: lista de puntos con nombre y fecha, elegir cuál restaurar — pero sigue sin abrir ninguno.
Serialización y costes
Cuando el memento debe persistir o viajar, se serializa (JSON, binario...). Avisos:
- La serialización es una puerta trasera a la encapsulación: el JSON del memento expone los campos que tanto protegimos, y quien pueda editarlo puede fabricar estados ilegales. Trátalo como frontera de confianza: al restaurar desde fuera, revalida (que las cantidades sean positivas, que el cupón exista aún...).
- Versionado: la foto de octubre debe poder abrirse con el código de diciembre. Añade un número de versión al formato y un camino de migración, o las instantáneas viejas se vuelven bombas.
- Coste de memoria/espacio: instantánea completa × histórico largo × objeto grande = problema. Mitigaciones: histórico acotado (nuestro
MAX_INSTANTANEAS), instantáneas incrementales (guardar diferencias — con lo que te acercas a Command), compartir las partes no cambiadas entre fotos (estructuras persistentes; si el estado es inmutable, "copiar" es compartir referencias, casi gratis — otro dividendo de la inmutabilidad que venimos cultivando desde el Builder).
Variantes
- Memento como clase anidada privada + interfaz marcadora: la versión GoF estricta; el caretaker tipa contra una interfaz vacía
Object-like. Nuestra clase anidada con constructor privado logra lo mismo con tipos más útiles. - Originator = caretaker interno: el propio objeto guarda su pila de estados (
carrito.deshacer()). Menos flexible (política de guardado fija) pero API más simple; razonable cuando solo hay un consumidor del undo. - Memento incremental: cada foto guarda solo el delta respecto a la anterior. Ahorra espacio, complica la restauración (hay que reproducir la cadena de deltas).
- Con estado inmutable: si el originator es inmutable (estilo
Pedidodel Builder), cada versión es su propio memento — el "histórico" es una lista de versiones y restaurar es apuntar a una vieja. El patrón se disuelve en la arquitectura; así funcionan los sistemas de gestión de estado de las UI modernas (Redux y compañía).
Cuándo usarlo y cuándo no
Úsalo cuando:
- Necesitas deshacer/restaurar estados y las operaciones inversas no existen, no son fiables o exigen guardar tanto contexto como la foto.
- Debes ofrecer puntos de control (checkpoints) o recuperación ante errores (capturar antes de una operación arriesgada, restaurar si falla).
- Y, condición irrenunciable: exponer el estado con getters/setters rompería la encapsulación que protege los invariantes del objeto.
Evítalo cuando:
- El estado es grande y cambia a menudo, y no puedes permitirte fotos completas ni la complejidad de los deltas.
- El objeto ya es inmutable o su estado es público y trivial: guarda referencias o copias sin liturgia de patrón.
- Solo necesitas deshacer operaciones con inversa obvia y barata: Command solo es más simple.
Relación con otros patrones
- Command: la pareja del undo robusto — el comando guarda en su
ejecutar()un memento del receiver, y sudeshacer()lo restaura. Inversa cuando es fácil, foto cuando no; se combinan sin fricción. - Prototype: capturar estado es, mecánicamente, clonar; las disciplinas de copia profunda son las mismas.
- Iterator: un iterador puede empaquetar su posición en un memento para pausar y retomar recorridos.
- State (siguiente parada): comparten la palabra "estado" pero no el problema — State cambia el comportamiento según el estado; Memento fotografía los datos. La aclaración completa, en la comparativa.
Errores comunes
- Fotos que comparten estructuras mutables con el original (faltó
List.copyOfo la copia profunda): el histórico entero apunta al presente y el deshacer no deshace nada. Es el error número uno; testéalo siempre (captura → muta → restaura → verifica). - Caretaker que abre el memento (campos package-private "por comodidad", getters añadidos "para un caso"): el patrón degenera en un DTO público y la encapsulación que venía a proteger desaparece.
- Histórico sin límite: la fuga de memoria del undo, agravada aquí porque cada entrada es una foto completa.
- Guardar dependencias o estado ajeno en la foto: al restaurar, revives referencias obsoletas (un servicio ya cerrado, el estado de otro objeto en versión antigua).
- Confiar en mementos deserializados sin revalidar: estado ilegal inyectado por la puerta de atrás.
Ejercicios
Ejercicio 1: la foto traidora
Un compañero implementa la captura así: this.lineas = origen.lineas; (sin copiar). Escribe la secuencia mínima de llamadas (captura, mutación, restauración) que demuestra el bug, y explica qué observará el usuario en la pantalla del carrito.
Ejercicio 2: puntos de restauración con nombre
Implementa HistorialCarta, caretaker para la edición de la carta: crearPunto(String nombre, SeccionCarta.Memento m), listarPuntos() (nombre + fecha, para el desplegable de la UI) y recuperar(String nombre). Recuerda: no puede mirar dentro de ningún memento. ¿De dónde salen el nombre y la fecha que lista?
Ejercicio 3: ¿inversa o foto?
Para cada operación del panel del restaurante, decide si su undo conviene por operación inversa (Command puro) o por memento, y justifica: (a) marcarEnPreparacion() ↔ volverAAceptado(); (b) "reequilibrar precios" (sube unos, baja otros, redondea, según reglas que cambian cada mes); (c) retrasarEntrega(minutos).
Soluciones
Solución 1:
Carrito.Memento foto = carrito.guardarEstado(); // "foto" (comparte la lista viva)
carrito.añadirLinea(pizzaCarbonara, 2); // muta la lista... también en la foto
carrito.restaurar(foto); // restaura... el estado ya mutadoLa restauración no restaura: la foto apuntaba a la misma List, así que la carbonara sigue en el carrito. El usuario pulsa "deshacer" y no pasa nada visible — el peor tipo de bug: sin excepción, sin log, solo un undo que miente. (Con el matiz de implementación: restaurar hace clear() + addAll() sobre esa misma lista compartida; según el orden de las operaciones puede incluso acabar vaciando la "foto". Cualquier variante del síntoma nace del mismo pecado: no copiar.)
Solución 2:
public class HistorialCarta {
private record Punto(String nombre, SeccionCarta.Memento memento) {}
private final Map<String, Punto> puntos = new LinkedHashMap<>();
public void crearPunto(String nombre, SeccionCarta.Memento memento) {
puntos.put(nombre, new Punto(nombre, memento));
}
public List<String> listarPuntos() {
return puntos.values().stream()
.map(p -> p.nombre() + " (" + p.memento().getMomento() + ")")
.toList();
}
public Optional<SeccionCarta.Memento> recuperar(String nombre) {
return Optional.ofNullable(puntos.get(nombre)).map(Punto::memento);
}
}El nombre lo aporta el caretaker (es metadato de gestión, no estado de la carta); la fecha sale del metadato público getMomento() del memento — la interfaz estrecha permite metadatos sin exponer contenido. La restauración en sí la hará el originator: carta.restaurar(memento).
Solución 3: (a) inversa — transición simétrica, barata y estable: Command puro basta; (b) memento — la "inversa" de un reequilibrio con reglas cambiantes y redondeos es prácticamente incomputable (los redondeos ni siquiera son biyectivos): foto de precios antes, restaurar si disgusta; (c) inversa (retrasarEntrega(-minutos)) como vimos en Command... salvo que el retraso dispare recálculos en cascada, en cuyo caso la foto vuelve a ganar. Moraleja: inversa para lo simétrico y barato; memento cuando la inversa exige reconstruir historia.
Conclusión
Memento resuelve un equilibrio que parecía imposible: el estado del carrito sale fuera —al histórico, a disco— sin que nadie salvo Carrito pueda leerlo ni fabricarlo, gracias a la doble interfaz (amplia para el originator, estrecha para el caretaker) y a la clase anidada con constructor privado que la materializa en Java. Te llevas también las decisiones que separan un undo de juguete de uno de producción: copiar de verdad al capturar, fotografiar el estado mínimo no derivable, acotar el histórico, y revalidar todo memento que haya viajado. Y la regla para elegir arma: inversa (Command) cuando es simétrica y barata; foto (Memento) cuando la inversa miente o no existe.
Con esto quedan saldadas las deudas del deshacer. La siguiente lección salda una mucho más antigua — la deuda del curso. En la primera lección viste un Pedido.cambiarEstado(...) que creaba con new el push, el SMS y las estadísticas, y prometimos que un patrón resolvería aquel entuerto con elegancia. Tres módulos después, el momento ha llegado: uno cambia, muchos se enteran, y nadie conoce a nadie. Nos vemos en Observer.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
