💾 El Patrón Memento: Viajando en el Tiempo
El Patrón de diseño Memento es un patrón de comportamiento con un objetivo a cumplir que todos queremos cuando la liamos: poder volver atrás. Su misión es guardar el estado de un objeto para poder restaurarlo después, pero sin romper las reglas de privacidad de ese objeto.
¿Cómo guardas una copia exacta de un objeto (incluyendo sus variables privadas) para poder restaurarlo luego, sin exponer esas variables privadas al exterior y romper el encapsulamiento?
Dicho de otra forma: dejas que el propio objeto cree una “instantánea” (Memento) de sí mismo. Solo él puede leerla, pero otros pueden guardarla y devolvérsela cuando haga falta.

🎮 1. Guardar Partida
Imagina que estás jugando a un videojuego difícil, super dificil tipo Elden Ring (El originator):
- Antes de enfrentarte al final boss, le das a Guardar Partida.
- El juego crea un archivo de guardado (El Memento) con tu salud, inventario, posición y progreso.
- Tú (El caretaker) guardas ese archivo en la Memory Card, no sabes qué hay dentro si bits, bytes o patatas fritas, solo sabes que es tu partida guardada.
- Si el jefe te mata, cargas el archivo y vuelves exactamente a como estabas. Sin hacer trampas, sin perder nada, sin dramas.
Tú (el caretaker) no puedes abrir el archivo de guardado y editarte 999 de vida. El archivo es opaco. Solo el juego (el originator) sabe interpretar y restaurar esos datos. Eso es encapsulamiento.
¿Qué tipo de patrón de diseño es Memento?
🛠️ 2. Los roles del patrón
El objeto cuyo estado queremos guardar y restaurar. Tiene dos métodos clave: guardar() para crear un memento y restaurar(m) para volver a un estado anterior.
Objeto de valor inmutable que contiene una instantánea del estado. Debe ser opaco para el caretaker (no puede ver ni modificar los datos) pero accesible para el originator.
Guarda los mementos en un historial (normalmente una pila). Nunca opera ni inspecciona el contenido del memento. Solo los almacena y los devuelve cuando se piden.
La clave del patrón es la responsabilidad de crear y leer el memento es exclusiva del originator. El caretaker es solo un almacén de mementos. No sabe qué hay dentro, no puede modificarlos, solo los guarda y devuelve.
¿Por qué el caretaker no debe modificar ni inspeccionar el contenido del Memento?
🧑💻 3. Ejemplo práctico en PHP: Editor de texto con Undo y Redo
Vamos a implementar un editor de texto con historial completo: no solo deshacer, sino también rehacer. El truco está en tener dos pilas: una de undo y otra de redo.
<?php
// 1. Memento: la instantánea del estado
class EditorMemento
{
public function __construct(
private readonly string $contenido,
private readonly int $posicionCursor,
private readonly \DateTimeImmutable $fecha
) {}
// Solo el originator debería usar estos getters
public function getContenido(): string
{
return $this->contenido;
}
public function getPosicionCursor(): int
{
return $this->posicionCursor;
}
public function getFecha(): \DateTimeImmutable
{
return $this->fecha;
}
}
// 2. originator: el editor que crea y restaura mementos
class EditorTexto
{
private string $contenido = '';
private int $posicionCursor = 0;
public function escribir(string $texto): void
{
// Inserta en la posición del cursor
$this->contenido = 2025-12-02-patron-diseno-memento.mdxsubstr($this->contenido, 0, $this->posicionCursor)
. $texto
. substr($this->contenido, $this->posicionCursor);
$this->posicionCursor += strlen($texto);
}
public function borrar(int $caracteres): void
{
if ($this->posicionCursor < $caracteres) {
$caracteres = $this->posicionCursor;
}
$this->contenido = 2025-12-02-patron-diseno-memento.mdxsubstr($this->contenido, 0, $this->posicionCursor - $caracteres)
. substr($this->contenido, $this->posicionCursor);
$this->posicionCursor -= $caracteres;
}
public function getContenido(): string
{
return $this->contenido;
}
// Crea un memento con el estado actual
public function guardar(): EditorMemento
{
return new EditorMemento(
$this->contenido,
$this->posicionCursor,
new \DateTimeImmutable()
);
}
// Restaura el estado desde un memento
public function restaurar(EditorMemento $memento): void
{
$this->contenido = $memento->getContenido();
$this->posicionCursor = $memento->getPosicionCursor();
}
}
// 3. caretaker: gestiona el historial con undo Y redo
class Historial
{
/** @var EditorMemento[] */
private array $pilaUndo = [];
/** @var EditorMemento[] */
private array $pilaRedo = [];
private EditorTexto $editor;
public function __construct(EditorTexto $editor)
{
$this->editor = $editor;
}
public function backup(): void
{
$this->pilaUndo[] = $this->editor->guardar();
// Al hacer una acción nueva, limpiamos el redo
$this->pilaRedo = [];
}
public function deshacer(): void
{
if (empty($this->pilaUndo)) {
echo " ⚠️ No hay nada que deshacer.\n";
return;
}
// Guardamos el estado actual en redo antes de restaurar
$this->pilaRedo[] = $this->editor->guardar();
$memento = array_pop($this->pilaUndo);
$this->editor->restaurar($memento);
}
public function rehacer(): void
{
if (empty($this->pilaRedo)) {
echo " ⚠️ No hay nada que rehacer.\n";
return;
}
$this->pilaUndo[] = $this->editor->guardar();
$memento = array_pop($this->pilaRedo);
$this->editor->restaurar($memento);
}
public function getNumVersiones(): int
{
return count($this->pilaUndo);
}
}
// 4. Uso
$editor = new EditorTexto();
$historial = new Historial($editor);
$editor->escribir("Hola");
$historial->backup();
echo "1. '{$editor->getContenido()}'\n";
$editor->escribir(" Mundo");
$historial->backup();
echo "2. '{$editor->getContenido()}'\n";
$editor->escribir(" Cruel");
echo "3. '{$editor->getContenido()}'\n";
echo "\n--- Undo ---\n";
$historial->deshacer();
echo " '{$editor->getContenido()}'\n";
$historial->deshacer();
echo " '{$editor->getContenido()}'\n";
echo "\n--- Redo ---\n";
$historial->rehacer();
echo " '{$editor->getContenido()}'\n";
// Salida:
// 1. 'Hola'
// 2. 'Hola Mundo'
// 3. 'Hola Mundo Cruel'
//
// --- Undo ---
// 'Hola Mundo'
// 'Hola'
//
// --- Redo ---
// 'Hola Mundo'
Cuando el usuario deshace y luego escribe algo nuevo, la pila de redo se vacía. Esto es exactamente lo que pasa en cualquier editor: si deshaces y luego escribes, ya no puedes rehacer lo antiguo. Eso es lo que hace $this->pilaRedo = [] en backup().
¿Qué debe pasar con la pila de Redo cuando el usuario realiza una acción nueva después de deshacer?
💳 4. Ejemplo real: Transacción con rollback
Otro caso de uso muy real: un proceso de compra donde necesitas hacer rollback si algo falla a mitad. Guardas el estado antes de cada paso y, si hay error, restauras:
<?php
class PedidoMemento
{
public function __construct(
private readonly string $estado,
private readonly float $total,
private readonly array $items
) {}
public function getEstado(): string { return $this->estado; }
public function getTotal(): float { return $this->total; }
public function getItems(): array { return $this->items; }
}
class Pedido
{
private string $estado = 'creado';
private float $total = 0.0;
private array $items = [];
public function agregarItem(string $item, float $precio): void
{
$this->items[] = ['nombre' => $item, 'precio' => $precio];
$this->total += $precio;
}
public function procesarPago(): void
{
$this->estado = 'pagado';
echo " 💳 Pago procesado: {$this->total}€\n";
}
public function enviar(): void
{
if ($this->estado !== 'pagado') {
throw new \RuntimeException("No se puede enviar sin pagar");
}
$this->estado = 'enviado';
echo " 📦 Pedido enviado\n";
}
public function guardar(): PedidoMemento
{
return new PedidoMemento($this->estado, $this->total, $this->items);
}
public function restaurar(PedidoMemento $memento): void
{
$this->estado = $memento->getEstado();
$this->total = $memento->getTotal();
$this->items = $memento->getItems();
echo " ↩️ Pedido restaurado a estado: '{$this->estado}'\n";
}
public function getEstado(): string { return $this->estado; }
public function getTotal(): float { return $this->total; }
}
// Uso: proceso de compra con rollback
$pedido = new Pedido();
$pedido->agregarItem("Teclado mecánico", 89.99);
$pedido->agregarItem("Ratón gaming", 49.99);
// Guardamos antes de procesar
$checkpoint = $pedido->guardar();
try {
$pedido->procesarPago();
// Simulamos que el envío falla
throw new \RuntimeException("Error: servicio de envío no disponible");
$pedido->enviar();
} catch (\RuntimeException $e) {
echo " ❌ Error: {$e->getMessage()}\n";
$pedido->restaurar($checkpoint);
echo " Estado actual: {$pedido->getEstado()}, Total: {$pedido->getTotal()}€\n";
}
// Salida:
// 💳 Pago procesado: 139.98€
// ❌ Error: Error: servicio de envío no disponible
// ↩️ Pedido restaurado a estado: 'creado'
// Estado actual: creado, Total: 139.98€
El pedido vuelve al estado creado como si el pago nunca hubiera pasado. Esto es Memento aplicado a transacciones. En el mundo real, lo combinas con un sistema de compensación para revertir el pago real.
🌍 5. Casos de uso en el mundo real
VS Code, Word, Photoshop... todos usan una variante de Memento para la función deshacer/rehacer. Cada acción guarda una instantánea del estado del documento.
Un BEGIN TRANSACTION + ROLLBACK es conceptualmente un Memento. La BD guarda el estado y lo restaura si algo falla. WAL (Write-Ahead Log) es la implementación interna.
Cada commit es un memento de tu código. Puedes volver a cualquier punto: git checkout abc123. El historial completo son mementos encadenados.
Cuando haces un snapshot en Docker, VMware o VirtualBox, guardas el estado completo de la máquina. Si algo sale mal, restauras el snapshot y como si nada.
🆚 6. Memento vs Command para Deshacer
Este es un debate clásico porque hay dos formas de implementar Deshacer y es importante entender cuándo usar cada una:
- Guarda una foto completa del estado
- Restaurar es instantáneo (solo copias el estado)
- Consume más memoria (una copia por cada versión)
- Ideal cuando el estado es pequeño o las operaciones son difíciles de invertir
- Guarda la operación + su inversa (ej: "insertar X" → "borrar X")
- Restaurar = ejecutar la operación inversa
- Consume menos memoria (solo la operación, no todo el estado)
- Ideal cuando el estado es grande y las operaciones son fáciles de invertir
En la práctica, muchos sistemas usan una combinación. Git, por ejemplo, guarda snapshots completos (mementos) pero usa deltas (diffs/commands) para optimizar espacio.
¿Cuál es la principal diferencia entre usar Memento y Command para deshacer?
🔄 7. Comparativa con otros patrones
| Patrón | Propósito | Diferencia clave |
|---|---|---|
| Memento | Guardar y restaurar el estado interno de un objeto | Guarda una instantánea completa del estado, respetando el encapsulamiento |
| Command | Encapsular una acción como objeto | Command puede deshacer ejecutando la inversa; Memento deshace restaurando el snapshot |
| Prototype | Clonar objetos | Prototype clona para crear nuevos objetos; Memento clona para poder volver atrás |
| State | Cambiar comportamiento según el estado | State cambia cómo se comporta un objeto; Memento guarda los datos del estado |
| Iterator | Recorrer colecciones | Un Iterator con guardarPosicion()/restaurarPosicion() usa internamente la idea de Memento |
⚖️ 8. Relación con SOLID
El originator se encarga de su lógica de negocio. El caretaker se encarga del historial. El Memento solo almacena datos. Tres responsabilidades bien separadas.
¿Quieres guardar más campos en el memento? Modificas el originator y el Memento. El caretaker no cambia porque no sabe qué hay dentro.
La gracia del patrón: el estado privado del originator nunca se expone. El Memento es opaco para el caretaker. Nadie puede hacer trampas.
El caretaker puede depender de una interfaz MementoInterface en vez del memento concreto. Así cambias la implementación sin tocar el historial.
¿Qué principio SOLID se respeta al separar la gestión del historial (caretaker) de la lógica del objeto (originator)?
✅ 9. Ventajas y desventajas
- Encapsulamiento intacto: Guardas el estado privado sin hacerlo público.
- Deshacer/Rehacer limpio: La implementación más elegante de undo/redo.
- SRP: El originator no gestiona el historial, eso lo hace el caretaker.
- Transacciones: Perfecto para implementar rollback en procesos complejos.
- Simple de entender: Tres roles claros, sin magia.
- Consumo de memoria: Si el estado es grande y guardas muchas copias, te puedes comer la RAM.
- Copias profundas: Si el estado contiene objetos anidados, necesitas deep clone para evitar referencias compartidas.
- Coste de serialización: Serializar/deserializar estados complejos puede ser costoso en tiempo.
- Sin límite: Sin control, el historial crece indefinidamente. Necesitas poner un tope.
¿Cuál es la principal desventaja del patrón Memento?
⚠️ 10. Errores comunes
1. No hacer deep copy del estado
Si el memento guarda referencias en vez de copias, al modificar el estado actual también modificas el memento. La instantánea queda corrompida:
// ❌ MAL: el memento guarda una referencia al array
class EditorMemento
{
private array $lineas;
public function __construct(array &$lineas) // ← Referencia
{
$this->lineas = &$lineas; // 💀 Apunta al mismo array
}
}
// ✅ BIEN: el memento guarda una copia
class EditorMemento
{
private array $lineas;
public function __construct(array $lineas) // ← Copia por valor
{
$this->lineas = $lineas; // PHP copia arrays por valor
}
}
En PHP los arrays se copian por valor, así que por defecto estás a salvo. Pero si el array contiene objetos, esos
objetos se pasan por referencia. Ahí necesitas clone o unserialize(serialize(...)).
2. Historial sin límite de tamaño
Si no pones un tope, el historial crece hasta reventar la memoria:
// ❌ MAL: crece sin control
class Historial
{
private array $mementos = []; // Sin límite → 💀 OOM
public function backup(): void
{
$this->mementos[] = $this->editor->guardar();
}
}
// ✅ BIEN: limita el historial
class Historial
{
private const MAX_VERSIONES = 50;
private array $mementos = [];
public function backup(): void
{
if (count($this->mementos) >= self::MAX_VERSIONES) {
array_shift($this->mementos); // Quita el más antiguo
}
$this->mementos[] = $this->editor->guardar();
}
}
3. Exponer el estado del Memento al caretaker
Si caretaker puede leer o modificar el contenido del memento, rompes el encapsulamiento y pierdes toda la gracia del patrón:
// ❌ MAL: caretaker manipula el memento
class Historial
{
public function deshacer(): void
{
$memento = array_pop($this->mementos);
// caretaker accede al contenido del memento
$contenido = $memento->getContenido(); // ← No debería poder
$contenido = strtoupper($contenido); // ← Lo modifica 💀
}
}
// ✅ BIEN: el caretaker solo almacena y devuelve
class Historial
{
public function deshacer(): void
{
$memento = array_pop($this->mementos);
$this->editor->restaurar($memento); // Lo devuelve al originator
}
}
En lenguajes como Java puedes hacer el memento una clase interna del originator para forzar la opacidad. En PHP hay que confiar más en la disciplina del equipo (o usar interfaces que restrinjan los métodos visibles).
4. Olvidar limpiar la pila de redo
Si el usuario deshace dos veces y luego escribe algo nuevo, la pila de redo debe vaciarse. Si no lo haces, al rehacer restauras un estado que ya no tiene sentido en la línea temporal actual:
// ❌ MAL: el redo mantiene estados obsoletos
public function backup(): void
{
$this->pilaUndo[] = $this->editor->guardar();
// No limpia pilaRedo → rehacer lleva a estados fantasma
}
// ✅ BIEN: acción nueva = redo se vacía
public function backup(): void
{
$this->pilaUndo[] = $this->editor->guardar();
$this->pilaRedo = []; // Reset
}
¿Qué problema ocurre si el Memento guarda referencias en vez de copias del estado?
🤔 11. ¿Cuándo usarlo y cuándo no?
- Necesitas deshacer/rehacer (editores, formularios, dibujo)
- Necesitas transacciones con rollback (procesos de compra, migraciones)
- Quieres guardar checkpoints en procesos largos o complejos
- El estado es privado y no quieres exponerlo con getters públicos
- Las operaciones son difíciles de invertir (más fácil restaurar que calcular la inversa)
- El estado es muy grande (imágenes, vídeos) y no puedes permitirte copias frecuentes
- Las operaciones son fácilmente invertibles (suma → resta). Ahí Command es mejor
- Solo necesitas un nivel de deshacer (guarda el último estado, sin historial)
- El objeto no tiene estado privado que proteger (es todo público)
EPA!: Si puedes describir tu necesidad como quiero volver atrás en el tiempo sin exponer las tripas del objeto, es un Memento.
¿Cuándo es mejor usar Command en vez de Memento para implementar deshacer?
💡 12. Conclusión
El Patrón Memento es tu salvación cuando necesitas implementar Deshacer, transacciones con rollback o checkpoints. Es la forma limpia de viajar en el tiempo dentro de tu aplicación sin romper el encapsulamiento.
Sus tres roles son simples: el originator crea y lee las instantáneas, el Memento las almacena de forma opaca, y el caretaker las gestiona sin cotillear. Cada uno a su rollo.
Eso sí, vigila la memoria. Si guardas una instantánea por cada pulsación de tecla y tu objeto pesa 5MB, vas a tener un problema. Pon límites al historial, usa snapshots incrementales si puedes, y valora si Command no es mejor opción cuando las operaciones son fácilmente invertibles.
EA, ¡saluditos y nos vemos en los bares! 🍻