Skip to content
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

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Patrón Memento

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

Preguntas Frecuentes

P: ¿Cómo se diferencia Memento de Prototype? R: Prototype crea un nuevo objeto copiando uno existente. Memento guarda el estado de un objeto para poder restaurarlo más tarde. Prototype es sobre duplicación; Memento es sobre viajar en el tiempo.

P: ¿Puedo usar serialización en lugar de Memento? R: Sí, pero la serialización es a menudo más lenta y menos controlada. Memento te da control granular sobre qué estado se guarda y cómo se restaura.

¿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.