StackPractices
intermediate Por Mathias Paulenko

Patrón Memento

Captura y restaura el estado interno de un objeto sin violar el encapsulamiento. Un patrón de comportamiento para deshacer/rehacer.

Temas: design

Visión General

El Patrón Memento es un patrón de diseño de comportamiento que te permite guardar y restaurar el estado anterior de un objeto sin revelar su estructura interna. Es la base de la funcionalidad de deshacer/rehacer en aplicaciones como editores de texto, programas de dibujo, y gestión de estado de juegos.

Cuándo Usarlo

Usa el Patrón Memento cuando:

  • Necesitas implementar funcionalidad de deshacer y rehacer
  • Quieres guardar puntos de control del estado de un objeto
  • Debes preservar el encapsulamiento y no exponer el estado interno directamente
  • La restauración de estado debería ser posible sin que el cliente conozca los internals del objeto

Solución

Python

class EditorMemento:
    def __init__(self, content: str, cursor: int):
        self._content = content
        self._cursor = cursor

    @property
    def content(self) -> str:
        return self._content

    @property
    def cursor(self) -> int:
        return self._cursor

class TextEditor:
    def __init__(self):
        self._content = ""
        self._cursor = 0

    def type(self, text: str):
        self._content = self._content[:self._cursor] + text
        self._cursor += len(text)

    def save(self) -> EditorMemento:
        return EditorMemento(self._content, self._cursor)

    def restore(self, memento: EditorMemento):
        self._content = memento.content
        self._cursor = memento.cursor

    @property
    def content(self) -> str:
        return self._content

# Uso con historial
class History:
    def __init__(self):
        self._history = []

    def push(self, memento):
        self._history.append(memento)

    def pop(self):
        if not self._history:
            return None
        return self._history.pop()

editor = TextEditor()
history = History()

history.push(editor.save())
editor.type("Hola ")
history.push(editor.save())
editor.type("Mundo!")

print(editor.content)  # Hola Mundo!

editor.restore(history.pop())
print(editor.content)  # Hola

editor.restore(history.pop())
print(editor.content)  # (vacío)

JavaScript

class EditorMemento {
  constructor(content, cursor) {
    this.content = content;
    this.cursor = cursor;
  }
}

class TextEditor {
  constructor() {
    this._content = "";
    this._cursor = 0;
  }

  type(text) {
    this._content = this._content.slice(0, this._cursor) + text;
    this._cursor += text.length;
  }

  save() {
    return new EditorMemento(this._content, this._cursor);
  }

  restore(memento) {
    this._content = memento.content;
    this._cursor = memento.cursor;
  }

  get content() {
    return this._content;
  }
}

// Uso
class History {
  constructor() {
    this.history = [];
  }

  push(memento) {
    this.history.push(memento);
  }

  pop() {
    return this.history.pop();
  }
}

const editor = new TextEditor();
const history = new History();

history.push(editor.save());
editor.type("Hola ");
history.push(editor.save());
editor.type("Mundo!");

console.log(editor.content); // Hola Mundo!

editor.restore(history.pop());
console.log(editor.content); // Hola

Java

public class EditorMemento {
    private final String content;
    private final int cursor;

    public EditorMemento(String content, int cursor) {
        this.content = content;
        this.cursor = cursor;
    }

    public String getContent() { return content; }
    public int getCursor() { return cursor; }
}

public class TextEditor {
    private String content = "";
    private int cursor = 0;

    public void type(String text) {
        content = content.substring(0, cursor) + text;
        cursor += text.length();
    }

    public EditorMemento save() {
        return new EditorMemento(content, cursor);
    }

    public void restore(EditorMemento memento) {
        this.content = memento.getContent();
        this.cursor = memento.getCursor();
    }

    public String getContent() { return content; }
}

// Uso con historial
import java.util.ArrayDeque;
import java.util.Deque;

public class History {
    private final Deque<EditorMemento> stack = new ArrayDeque<>();

    public void push(EditorMemento memento) {
        stack.push(memento);
    }

    public EditorMemento pop() {
        return stack.isEmpty() ? null : stack.pop();
    }
}

// Demo
TextEditor editor = new TextEditor();
History history = new History();

history.push(editor.save());
editor.type("Hola ");
history.push(editor.save());
editor.type("Mundo!");

System.out.println(editor.getContent());

editor.restore(history.pop());
System.out.println(editor.getContent());

Explicación

El Patrón Memento tiene tres roles:

  • Originador (TextEditor): El objeto cuyo estado necesita ser guardado
  • Memento (EditorMemento): Una instantánea inmutable del estado del originador
  • Cuidador (History): Gestiona los mementos (cuándo guardar, cuándo restaurar) sin acceder a su contenido

El beneficio clave es que el estado interno del memento es opaco al cuidador, preservando el encapsulamiento.

Variantes

VarianteDescripciónCaso de Uso
Instantánea CompletaAlmacena todo el estado del objetoObjetos pequeños, instantáneas infrecuentes
Delta/IncrementalAlmacena solo campos cambiadosObjetos grandes, instantáneas frecuentes
Memento SerializableUsa serialización para copia profundaGrafos de objetos complejos
Command + MementoComandos almacenan mementos para deshacerSistemas transaccionales, editores

Lo que Funciona

  • Mantén los mementos inmutables después de la creación para prevenir manipulación accidental
  • Limita la vida útil de los mementos — historiales grandes consumen memoria sustancial
  • Considera serialización para grafos de objetos complejos, pero sé consciente de los costos de rendimiento
  • Implementa una interfaz de memento que solo expone métodos de restauración de estado al originador
  • Usa mementos delta para objetos grandes donde solo cambian algunos campos

Errores Comunes

  • Exponer el estado interno del memento al cuidador, rompiendo el encapsulamiento
  • Almacenar demasiadas instantáneas completas, causando uso excesivo de memoria
  • No manejar versionado de mementos cuando la estructura del originador cambia con el tiempo
  • Olvidar validar mementos antes de la restauración (instantáneas corruptas o incompatibles)
  • Permitir que los originadores modifiquen mementos después de la creación, causando comportamiento impredecible de deshacer

Lectura Adicional

  • Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
  • Guías relacionadas: explora las guías de memento y pattern para profundizar.
  • Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
  • Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica patrón memento 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.

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.

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.