El patrón Memento permite capturar y externalizar el estado interno de un objeto sin violar su encapsulamiento, de modo que pueda ser restaurado posteriormente. Se fundamenta en almacenar instantáneas (mementos) que contienen la información necesaria para volver a un punto anterior.
Es habitual en editores, motores de reglas, sistemas de transacciones y juegos donde se requiere deshacer acciones, soportar checkpoints o auditar evoluciones de estado.
Cuando deseamos implementar undo/redo o puntos de restauración, aparece la necesidad de capturar el estado de objetos complejos. Guardar una copia superficial puede resultar insuficiente si existen referencias internas; exponer getters para todos los atributos compromete la encapsulación. Memento aborda el problema delegando la captura del estado al propio originador, que conoce su estructura interna.
El patrón evita duplicar lógica de serialización en el cliente y permite que la restauración se realice de forma segura incluso si la implementación del objeto cambia.
La intención es capturar y externalizar el estado interno de un objeto para poder restaurarlo más adelante sin exponer detalles de implementación. Memento es pertinente cuando:
El patrón incentiva separar el código de negocio del manejo de historiales, facilitando pruebas y mantenimiento.
Los elementos principales son:
Algunas implementaciones utilizan mementos serializables o comprimidos para optimizar almacenamiento.
Existen dos estrategias comunes:
La elección depende de la frecuencia de cambios, el tamaño del estado y los requisitos de rendimiento.
Un hospital digitaliza historias clínicas. El personal necesita registrar cambios, revisar versiones y revertir errores sin perder datos. Cada historia incluye datos personales, diagnósticos, medicación y notas sensibles; la estructura evoluciona con el tiempo. Memento permite capturar estados que pueden persistirse y restaurarse asegurando integridad y confidencialidad.
El siguiente ejemplo en C# modela un originador que representa una historia clínica y un caretaker que administra versiones:
using System;
using System.Collections.Generic;
using System.Text;
public class HistoriaClinica
{
private string _paciente;
private string? _diagnostico;
private List<string> _medicacion = new();
private string? _notas;
private DateTime _ultimaActualizacion;
public HistoriaClinica(string paciente)
{
_paciente = paciente ?? throw new ArgumentNullException(nameof(paciente));
_ultimaActualizacion = DateTime.UtcNow;
}
public void ActualizarDiagnostico(string diagnostico)
{
_diagnostico = diagnostico;
_ultimaActualizacion = DateTime.UtcNow;
}
public void AgregarMedicacion(string medicamento)
{
_medicacion.Add(medicamento);
_ultimaActualizacion = DateTime.UtcNow;
}
public void ActualizarNotas(string notas)
{
_notas = notas;
_ultimaActualizacion = DateTime.UtcNow;
}
public Memento Guardar()
{
return new Memento(
_paciente,
_diagnostico,
new List<string>(_medicacion),
_notas,
_ultimaActualizacion
);
}
public void Restaurar(Memento memento)
{
_paciente = memento.Paciente;
_diagnostico = memento.Diagnostico;
_medicacion = new List<string>(memento.Medicacion);
_notas = memento.Notas;
_ultimaActualizacion = memento.UltimaActualizacion;
}
public string Resumen()
{
var builder = new StringBuilder();
builder.AppendLine($"Paciente: {_paciente}");
builder.AppendLine($"Diagnostico: {_diagnostico}");
builder.AppendLine($"Medicacion: {string.Join(", ", _medicacion)}");
builder.AppendLine($"Notas: {_notas}");
builder.AppendLine($"Actualizado: {_ultimaActualizacion:u}");
return builder.ToString();
}
public record Memento(
string Paciente,
string? Diagnostico,
IReadOnlyList<string> Medicacion,
string? Notas,
DateTime UltimaActualizacion
);
}
public class GestorVersiones
{
private readonly Stack<HistoriaClinica.Memento> _historialDeshacer = new();
private readonly Stack<HistoriaClinica.Memento> _historialRehacer = new();
public void Respaldar(HistoriaClinica historia)
{
_historialDeshacer.Push(historia.Guardar());
_historialRehacer.Clear();
}
public void Deshacer(HistoriaClinica historia)
{
if (_historialDeshacer.Count == 0)
{
throw new InvalidOperationException("No hay versiones para deshacer");
}
var estadoActual = historia.Guardar();
_historialRehacer.Push(estadoActual);
historia.Restaurar(_historialDeshacer.Pop());
}
public void Rehacer(HistoriaClinica historia)
{
if (_historialRehacer.Count == 0)
{
throw new InvalidOperationException("No hay versiones para rehacer");
}
_historialDeshacer.Push(historia.Guardar());
historia.Restaurar(_historialRehacer.Pop());
}
}
public static class AplicacionMemento
{
public static void Main()
{
var historia = new HistoriaClinica("Lucia Fernandez");
var gestor = new GestorVersiones();
gestor.Respaldar(historia);
historia.ActualizarDiagnostico("Alergia estacional");
historia.AgregarMedicacion("Antihistaminicos");
gestor.Respaldar(historia);
historia.ActualizarNotas("Paciente reporta mejoras.");
Console.WriteLine(historia.Resumen());
gestor.Deshacer(historia);
Console.WriteLine("Tras deshacer:");
Console.WriteLine(historia.Resumen());
gestor.Rehacer(historia);
Console.WriteLine("Tras rehacer:");
Console.WriteLine(historia.Resumen());
}
}
El originador crea mementos completos cada vez que se respalda. El gestor de versiones actúa como caretaker: administra pilas de deshacer y rehacer sin inspeccionar los detalles del memento. Al deshacer o rehacer, la historia se restaura a estados anteriores sin exponer sus estructuras internas.
Separar la captura y la restauración en el originador garantiza que cambios futuros en la representación interna (por ejemplo, nuevas secciones) no afecten a los clientes del memento.
En .NET es habitual que editores WPF, WinUI o MAUI mantengan pilas de undo/redo respaldadas por mementos para preservar el encapsulamiento de los view models. Ejemplos como el CommunityToolkit.Mvvm muestran caretakers que administran instantáneas de propiedades.
Memento puede adaptarse a distintos escenarios:
Guardar mementos completos con demasiada frecuencia puede consumir memoria y degradar el rendimiento. También es riesgoso permitir que el caretaker modifique el contenido del memento, ya que podría introducir inconsistencias.
Otro error común es acoplar el memento a la interfaz pública del originador, exponiendo getters o setters que contradicen el encapsulamiento. Es preferible utilizar clases internas o interfaces privadas para mantener el control.
Memento se relaciona con Command cuando se necesitan revertir ejecuciones; los comandos pueden capturar mementos antes de actuar. También trabaja con Prototype para clonar estados complejos y con Iterator para marcar checkpoints durante recorridos. En sistemas con State, los mementos permiten volver a estados previos sin reconstruir toda la máquina de estados.