Memento Pattern para Snapshot y Restauracion de Estado
Captura y externaliza el estado interno de un objeto sin violar encapsulacion, habilitando undo, serializacion y rollback de estado en aplicaciones
El Memento pattern captura y externaliza el estado interno de un objeto para que el objeto pueda ser restaurado a ese estado mas tarde, sin violar encapsulacion. A diferencia de Command, que almacena operaciones, Memento almacena el snapshot completo del estado. Es la base para sistemas de undo, checkpoints en juegos y guardado de borradores en editores.
Cuando Usar Esto
- Necesitas restauracion completa de estado, no solo reversion de operaciones
- La estructura interna del objeto es compleja y deberia permanecer oculta
- Los snapshots deben persistirse a disco o transmitirse por red
Problema
Una aplicacion de dibujo necesita funcionalidad undo, pero exponer coordenadas y estilos de formas internas viola encapsulacion. Almacenar operaciones es insuficiente porque las formas pueden ser modificadas por herramientas externas.
Solucion
// memento/EditorMemento.ts
interface EditorMemento {
getState(): string;
}
class TextEditor {
private content = '';
private cursorPosition = 0;
private selectionRange: [number, number] = [0, 0];
type(text: string): void {
const before = this.content.slice(0, this.cursorPosition);
const after = this.content.slice(this.cursorPosition);
this.content = before + text + after;
this.cursorPosition += text.length;
this.selectionRange = [this.cursorPosition, this.cursorPosition];
}
delete(): void {
const [start, end] = this.selectionRange;
if (start === end && start > 0) {
this.content = this.content.slice(0, start - 1) + this.content.slice(start);
this.cursorPosition = start - 1;
} else {
this.content = this.content.slice(0, start) + this.content.slice(end);
this.cursorPosition = start;
}
this.selectionRange = [this.cursorPosition, this.cursorPosition];
}
// Crear snapshot
save(): EditorMemento {
return new EditorSnapshot(
this.content,
this.cursorPosition,
this.selectionRange
);
}
// Restaurar desde snapshot
restore(memento: EditorMemento): void {
const snapshot = memento as EditorSnapshot;
this.content = snapshot.getContent();
this.cursorPosition = snapshot.getCursor();
this.selectionRange = snapshot.getSelection();
}
getContent(): string {
return this.content;
}
}
// Memento implementation (opaca para clientes)
class EditorSnapshot implements EditorMemento {
constructor(
private content: string,
private cursor: number,
private selection: [number, number]
) {}
getState(): string {
return JSON.stringify({ content: this.content, cursor: this.cursor, selection: this.selection });
}
getContent(): string { return this.content; }
getCursor(): number { return this.cursor; }
getSelection(): [number, number] { return this.selection; }
}
// Caretaker maneja historial
class EditorHistory {
private history: EditorMemento[] = [];
private currentIndex = -1;
backup(editor: TextEditor): void {
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(editor.save());
this.currentIndex++;
}
undo(editor: TextEditor): void {
if (this.currentIndex <= 0) return;
this.currentIndex--;
editor.restore(this.history[this.currentIndex]);
}
redo(editor: TextEditor): void {
if (this.currentIndex >= this.history.length - 1) return;
this.currentIndex++;
editor.restore(this.history[this.currentIndex]);
}
}
// Uso
const editor = new TextEditor();
const history = new EditorHistory();
history.backup(editor);
editor.type('Hello');
history.backup(editor);
editor.type(' World');
console.log(editor.getContent()); // "Hello World"
history.undo(editor);
console.log(editor.getContent()); // "Hello"
history.redo(editor);
console.log(editor.getContent()); // "Hello World"
Variacion: Sistema de Checkpoints de Juego
// memento/GameCheckpoint.ts
class GameState {
private level = 1;
private health = 100;
private inventory: string[] = [];
createCheckpoint(): GameCheckpoint {
return new GameCheckpoint(this.level, this.health, [...this.inventory]);
}
loadCheckpoint(checkpoint: GameCheckpoint): void {
this.level = checkpoint.getLevel();
this.health = checkpoint.getHealth();
this.inventory = checkpoint.getInventory();
}
}
class GameCheckpoint implements EditorMemento {
constructor(
private level: number,
private health: number,
private inventory: string[]
) {}
getState(): string {
return JSON.stringify({ level: this.level, health: this.health });
}
getLevel(): number { return this.level; }
getHealth(): number { return this.health; }
getInventory(): string[] { return [...this.inventory]; }
}
Como Funciona
- Originator crea y restaura mementos de su propio estado
- Memento almacena el snapshot de estado; solo el Originator puede leerlo
- Caretaker maneja el historial de mementos sin acceder a sus contenidos
- Client solicita backups y dispara undo/redo a traves del Caretaker
Consideraciones de Produccion
- Usa comparticion estructural o delta encoding para estados grandes para reducir memoria
- Limita la profundidad del historial para prevenir crecimiento sin bounds
- Serializa mementos a JSON para persistencia entre sesiones
Errores Comunes
- Permitir que mementos sean modificados despues de la creacion, corrompiendo el historial
- Almacenar referencias a objetos mutables en lugar de deep copies
- Romper encapsulacion exponiendo internos de memento al Caretaker
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Temas Avanzados
Escenario: Memento para Editor de Formularios
// Memento pattern: guardar y restaurar estado sin exponer internals
type FormState = {
fields: Record<string, string>;
errors: Record<string, string>;
step: number;
}
class FormMemento {
constructor(private state: FormState) {}
getState(): FormState { return { ...this.state }; }
}
// Originator: el formulario
class FormEditor {
private state: FormState = { fields: {}, errors: {}, step: 0 };
setField(name: string, value: string) {
this.state.fields[name] = value;
}
setStep(step: number) { this.state.step = step; }
setErrors(errors: Record<string, string>) { this.state.errors = errors; }
save(): FormMemento {
return new FormMemento({ ...this.state, fields: { ...this.state.fields }, errors: { ...this.state.errors } });
}
restore(memento: FormMemento) {
this.state = memento.getState();
}
getState(): FormState { return { ...this.state }; }
}
// Caretaker: historial de estados
class FormHistory {
private history: FormMemento[] = [];
private maxHistory = 50;
push(memento: FormMemento) {
this.history.push(memento);
if (this.history.length > this.maxHistory) this.history.shift();
}
pop(): FormMemento | null { return this.history.pop() || null; }
canRestore(): boolean { return this.history.length > 0; }
}
// Uso: wizard form multi-step
const form = new FormEditor();
const history = new FormHistory();
// Step 1
form.setField("email", "user@test.com");
form.setStep(1);
history.push(form.save());
// Step 2
form.setField("name", "Alice");
form.setField("phone", "555-1234");
form.setStep(2);
history.push(form.save());
// Step 3
form.setField("address", "123 Main St");
form.setStep(3);
// User clicks "Back"
const prev = history.pop();
if (prev) form.restore(prev);
console.log(form.getState().step); // 2
console.log(form.getState().fields.name); // "Alice"
Lecciones:
- Memento guarda estado sin exponer internals de la clase
- El originator crea snapshots inmutables
- El caretaker gestiona el historial sin conocer el contenido
- Deep copy del estado: evitar mutaciones accidentales
- Limitar historial: 50 snapshots para no consumir memoria
- Memento vs Command: Memento guarda estado completo; Command guarda la operacion
### Memento vs Command para undo: cual uso?
Usa Memento cuando necesitas restaurar estado completo (formularios, editores visuales). Usa Command cuando necesitas deshacer operaciones individuales (texto, acciones). Memento es mas simple pero consume mas memoria: guarda todo el estado. Command es mas eficiente: solo guarda lo necesario para deshacer. Para wizards y formularios multi-step, Memento es ideal. Para editores de texto, Command es preferible.
## Puntos Clave
- **Aplica memento pattern para snapshot y restauracion de estado** cuando necesites una solución práctica para tu caso de uso.
- **Monitorea el rendimiento** después de implementar; mide latencia, errores y uso de recursos antes y después.
- **Revisa la sección de Troubleshooting** ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- **Mantén dependencias actualizadas** y ejecuta tests en CI para prevenir regresiones en producción.
## Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón. Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Command Pattern con Undo/Redo en TypeScript
Implementa el Command pattern para encapsular peticiones como objetos, habilitando operaciones undo/redo, encolamiento de peticiones y logging de operaciones
PatternPrototype Pattern para Clonacion de Objetos y Plantillas
Crea nuevos objetos copiando existentes, permitiendo plantillas pre-configuradas y evitando explosion de subclases cuando la creacion de objetos es costosa
PatternInterpreter Pattern para Lenguajes de Expresion
Construye un interprete de lenguaje que evalua expresiones y reglas representando la gramatica como objetos componibles, util para formulas, queries y reglas de negocio
PatternIterator Pattern para Traversal de Colecciones Custom en
Provee una forma de acceder a elementos de un objeto agregado secuencialmente sin exponer su representacion subyacente usando el Iterator pattern
PatternMediator Pattern para Desacoplamiento de Componentes en
Reduce dependencias caoticas entre componentes UI introduciendo un mediador que centraliza comunicacion, previniendo referencias explicitas entre pares