26. Memento (Recuerdo) - Patrón de Comportamiento

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.

26.1 Problema y contexto

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.

26.2 Intención y motivación

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:

  • Se necesita un historial de cambios para deshacer o rehacer operaciones.
  • El objeto tiene estructura interna compleja que no debe ser visible para el cliente.
  • Se desea implementar snapshots para auditorías, replicación o simulaciones.
  • Es necesario comparar estados (por ejemplo, para detectar divergencias) sin romper el encapsulamiento.

El patrón incentiva separar el código de negocio del manejo de historiales, facilitando pruebas y mantenimiento.

26.3 Participantes y estructura

Los elementos principales son:

  • Originator: objeto cuyo estado se desea capturar. Sabe cómo crear y restaurar mementos.
  • Memento: representación inmutable del estado interno. Puede dividirse en interfaces públicas y privadas para preservar encapsulamiento.
  • Caretaker: responsable de almacenar y administrar los mementos sin manipular su contenido.

Algunas implementaciones utilizan mementos serializables o comprimidos para optimizar almacenamiento.

26.4 Instantáneas completas vs. incrementales

Existen dos estrategias comunes:

  • Mementos completos: guardan todo el estado, garantizando restauración exacta pero consumiendo más memoria.
  • Mementos incrementales: almacenan solo las diferencias respecto de la instantánea anterior, reduciendo espacio a costa de mayor complejidad para reconstruir.

La elección depende de la frecuencia de cambios, el tamaño del estado y los requisitos de rendimiento.

26.5 Escenario: editor de historias clínicas

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.

26.6 Implementación en C#

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());
    }
}

26.7 Explicación del flujo

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.

26.8 Memento en el ecosistema .NET

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.

26.9 Variantes y extensiones

Memento puede adaptarse a distintos escenarios:

  • Mementos fuera de proceso: se serializan para enviarse a servicios remotos o almacenarse en sistemas de backup.
  • Mementos diferenciales: se apoyan en estructuras persistentes (por ejemplo, árboles inmutables) para compartir partes comunes entre versiones.
  • Mementos cifrados: protegen estados sensibles cuando el caretaker no debe acceder al contenido.

26.10 Riesgos y malas prácticas

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.

26.11 Buenas prácticas para aplicar Memento

  • Planificar una estrategia de almacenamiento: limitar la cantidad de mementos, comprimirlos o archivarlos.
  • Documentar el ciclo de vida de los mementos para evitar fugas de memoria.
  • Combinar Memento con eventos de auditoría para rastrear quién y cuándo dispara la restauración.
  • Utilizar pruebas que comparen estados antes y después de restaurar para validar consistencia.

26.12 Relación con otros patrones

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.