Programación · Patrones de diseño
Patrón Memento explicado con ejemplos
Respuesta corta
Memento es un patrón de diseño de comportamiento que captura el estado interno de un objeto en una instantánea, llamada memento, para poder restaurar el objeto a ese estado más tarde. El objeto que posee el estado (el originador) crea y restaura mementos, mientras que otro objeto (el cuidador) solo los almacena y nunca mira dentro. Es la forma estándar de implementar deshacer, puntos de control y partidas guardadas sin romper el encapsulamiento.
Intención y el problema que resuelve
Intención: sin violar el encapsulamiento, capturar y externalizar el estado interno de un objeto para que pueda restaurarse a ese estado más tarde. Memento es uno de los patrones de comportamiento del libro de la Banda de los Cuatro (Gang of Four, GoF).
Imagina una aplicación de dibujo donde una figura tiene posición, tamaño, color y una lista de valores privados derivados usados para el renderizado. Quieres un botón de deshacer. El gestor de deshacer podría copiar él mismo los campos de la figura, pero entonces debe conocer cada campo privado, y cada vez que cambia la clase de la figura, el gestor de deshacer se rompe. Hacer públicos todos los campos solo para deshacer permitiría que cualquier código dejara la figura en un estado inválido.
Memento lo resuelve dejando que la propia figura produzca una instantánea. Solo la figura sabe qué hay dentro de la instantánea y cómo restaurarse a partir de ella. El gestor de deshacer trata las instantáneas como objetos opacos: las guarda en una lista y devuelve una cuando el usuario pulsa deshacer.
Estructura y participantes
| Rol | Responsabilidad |
|---|---|
| Originador | El objeto cuyo estado se guarda; crea un memento de su estado actual y se restaura a partir de un memento |
| Memento | Una instantánea inmutable del estado del originador; no expone nada útil a otros objetos |
| Cuidador | Decide cuándo guardar y cuándo restaurar, y conserva los mementos (por ejemplo en una pila), pero nunca lee ni cambia su contenido |
La idea clave son dos interfaces: una ancha para el originador, que puede leer el estado guardado, y una estrecha para todos los demás, que solo pueden sostener el memento.
Implementación en TypeScript
La clase memento usa campos privados de JavaScript (#state), de modo que el cuidador no puede leer los datos almacenados desde el objeto memento. La lectura pasa por el método estático EditorMemento.read, al que solo llama el editor. En un proyecto real, pon la clase memento en el mismo módulo que el editor y no la exportes, para que otros módulos puedan sostener mementos pero nunca abrirlos.
type EditorState = { text: string; cursor: number };
class EditorMemento {
readonly #state: EditorState;
constructor(state: EditorState) {
this.#state = { ...state }; // copy, so later edits do not change the snapshot
}
static read(m: EditorMemento): EditorState {
return { ...m.#state };
}
}
class Editor {
private state: EditorState = { text: "", cursor: 0 };
type(words: string) {
const text = this.state.text + words;
this.state = { text, cursor: text.length };
}
get text() { return this.state.text; }
save(): EditorMemento { return new EditorMemento(this.state); }
restore(m: EditorMemento) { this.state = EditorMemento.read(m); }
}
class History {
private stack: EditorMemento[] = [];
constructor(private editor: Editor) {}
backup() { this.stack.push(this.editor.save()); }
undo() { const m = this.stack.pop(); if (m) this.editor.restore(m); }
}
const editor = new Editor();
const history = new History(editor);
history.backup(); editor.type("Hello");
history.backup(); editor.type(" world");
console.log(editor.text); // Hello world
history.undo();
console.log(editor.text); // Hello
history.undo();
console.log(JSON.stringify(editor.text)); // ""History es el cuidador. Llama a backup() antes de cada cambio y a undo() para volver atrás, pero nunca ve text ni cursor. Si más adelante añades un rango de selección a EditorState, History no cambia.
Memento frente a Command para deshacer
Hay dos formas habituales de implementar deshacer, y a menudo se combinan:
| Enfoque | Qué se guarda | Conviene cuando | Coste |
|---|---|---|---|
| Memento | Una instantánea de todo el estado antes de cada cambio | Los cambios son difíciles de revertir, o el estado es pequeño | La memoria crece con el tamaño del estado por el número de pasos |
| Command | La operación y datos suficientes para revertirla | Cada operación tiene una inversa simple | Cada comando necesita una lógica de deshacer correcta |
Un compromiso práctico: guardar comandos en la mayoría de los pasos y un memento completo cada N pasos, o guardar solo la parte del estado que cambia un comando.
Cuándo usarlo y cuándo no
- Úsalo para deshacer, rehacer, puntos de control, partidas guardadas y botones de "cancelar" en formularios que deben revertir las ediciones.
- Úsalo para transacciones en memoria: toma una instantánea antes de una operación arriesgada y restaura si falla.
- Úsalo cuando el cuidador no deba depender de los campos internos del originador.
- No lo uses cuando el estado sea muy grande y cambie a menudo, a menos que limites la longitud del historial o guardes solo las diferencias.
- No añadas toda la ceremonia cuando el estado ya es un valor inmutable simple. Con datos inmutables, un valor antiguo ya es una instantánea, y basta con conservar una lista de valores anteriores.
Usos en el mundo real
- La API History del navegador:
history.pushState(state, "", url)guarda un objeto de estado para cada entrada del historial, y el eventopopstatelo devuelve cuando el usuario navega. - Redux y almacenes similares conservan instantáneas inmutables del estado, que las herramientas usan para depurar viajando en el tiempo.
- Las bases de datos SQL admiten
SAVEPOINTdentro de una transacción, de modo que el trabajo puede revertirse hasta un punto con nombre. - Los editores de texto, los programas de gráficos y los juegos guardan instantáneas para deshacer, autoguardado y ranuras de guardado.
Patrones relacionados
| Patrón | Cómo se relaciona con Memento |
|---|---|
| Command | Command puede deshacer revirtiendo una operación; también puede guardar un memento tomado antes de ejecutarse y restaurarlo en undo. |
| Prototype | Prototype clona un objeto completo para crear uno nuevo; un memento guarda el estado para restaurar más tarde el mismo objeto. |
| Iterator | Un iterador puede exponer su posición actual como un memento para que un recorrido pueda reanudarse más tarde. |
| State | State cambia el comportamiento del objeto cuando cambia su estado; Memento solo registra y restaura los datos. |
Preguntas frecuentes
¿Cuál es la diferencia entre originador y cuidador?
El originador posee el estado y es el único objeto que puede crear un memento y restaurarse a partir de él. El cuidador decide cuándo guardar y restaurar y conserva los mementos, pero los trata como opacos.
¿Memento rompe el encapsulamiento?
No, ese es su propósito. Los campos internos del originador siguen siendo privados, y el memento los oculta al cuidador, así que solo el originador puede leer el estado guardado.
¿Cómo limito el uso de memoria con Memento?
Mantén un número máximo de instantáneas y descarta las más antiguas, guarda solo la parte cambiada del estado, o combina mementos con comandos que registren cambios pequeños.
¿La serialización JSON es un memento?
Serializar el estado a JSON puede ser el formato de almacenamiento de un memento, y es útil para guardar en disco. El patrón en sí trata de quién crea, conserva y restaura la instantánea, no del formato.