Перейти к содержанию

Memento (Снимок, Хранитель)

Категория: паттерн поведения.

Проблема

Нужно сохранять состояния объекта для undo, отката транзакции или контрольной точки. Если история сама читает и записывает поля объекта, приватное устройство раскрывается наружу, а любое изменение этих полей заставляет переделывать код истории.

Решение

Сам объект-создатель (Originator) формирует неизменяемый снимок своего закрытого состояния и умеет восстановиться из него. Опекун (Caretaker) решает, когда сохранить или вернуть состояние, но видит только ограниченный контракт снимка и не может менять его содержимое.

Структура

  • Originator - создаёт снимки и восстанавливает собственное состояние.
  • Memento - хранит состояние создателя; обычно неизменяем.
  • Caretaker - хранит историю снимков и управляет undo/redo, не заглядывая внутрь.

Варианты реализации

  • Вложенный класс снимка - естественный вариант для C#: закрытый вложенный тип имеет доступ к устройству создателя, а опекун получает лишь публичный ограниченный интерфейс.
  • Промежуточный интерфейс - создатель работает с конкретным снимком, опекун - с пустым или метаданным интерфейсом. Подходит, когда вложенный класс неудобен.
  • Снимок с методом Restore() - снимок сам знает, как восстановить связанный создатель. Это сильнее связывает их, но позволяет опекуну быть полностью независимым от типа создателя.
  • Полный или инкрементальный снимок - полный проще и надёжнее, инкрементальный хранит только изменения и экономит память ценой более сложного восстановления.

Когда применять

  • Нужны undo/redo, история изменений, контрольные точки или откат неудачной операции.
  • Прямое получение состояния нарушило бы инкапсуляцию объекта.
  • Состояние достаточно компактно либо предусмотрены ограничения истории и инкрементальные снимки.

Плюсы

  • Сохраняет инкапсуляцию: только создатель знает формат своего состояния.
  • Ответственность за хранение истории отделена от бизнес-объекта.
  • Упрощает отмену сложной операции, особенно вместе с Command.

Минусы

  • Частые полные снимки больших графов потребляют много памяти.
  • Опекун должен управлять временем жизни и очисткой истории.
  • Снимок ссылочного графа требует осознанного выбора между поверхностной и глубокой копией.

Отличие от Command и Prototype

Command хранит действие и может реализовать undo обратной операцией. Memento хранит состояние до действия; вместе они дают надёжную отмену. Prototype создаёт новый полноценный объект-клон, а Memento создаёт закрытый контейнер состояния специально для последующего восстановления исходного объекта.

Пример в .NET

  • Change tracking в ORM и история состояния редакторов используют близкие идеи, хотя их реализации не обязаны буквально повторять структуру GoF.
  • Сериализация объекта может быть техническим способом получить снимок, но сама по себе сериализация не является паттерном Memento и часто слишком тяжела для частого undo.

Источник для сверки структуры и вариантов: Refactoring.Guru - Memento.

Пример реализации на C#

Memento.cs
using System;
using System.Collections.Generic;

namespace DesignPatterns.Behavioral.Memento
{
    // Ограниченный контракт: хранитель может держать снимок, но не читать состояние.
    public interface IEditorMemento
    {
        DateTime CreatedAt { get; }
    }

    // Originator - единственный класс, который знает внутреннее устройство снимка.
    public sealed class TextEditor
    {
        private string _text = string.Empty;
        private int _cursor;

        public void SetState(string text, int cursor)
        {
            _text = text;
            _cursor = cursor;
        }

        public string Describe() => $"'{_text}', курсор: {_cursor}";

        public IEditorMemento Save() => new EditorMemento(_text, _cursor);

        public void Restore(IEditorMemento memento)
        {
            if (memento is not EditorMemento snapshot)
                throw new ArgumentException("Снимок создан другим объектом.", nameof(memento));

            _text = snapshot.Text;
            _cursor = snapshot.Cursor;
        }

        private sealed class EditorMemento : IEditorMemento
        {
            public EditorMemento(string text, int cursor)
            {
                Text = text;
                Cursor = cursor;
                CreatedAt = DateTime.UtcNow;
            }

            public string Text { get; }
            public int Cursor { get; }
            public DateTime CreatedAt { get; }
        }
    }

    // Caretaker - управляет историей, не раскрывая содержимое снимков.
    public sealed class EditorHistory
    {
        private readonly Stack<IEditorMemento> _undo = new();

        public void Backup(TextEditor editor) => _undo.Push(editor.Save());

        public bool Undo(TextEditor editor)
        {
            if (_undo.Count == 0)
                return false;

            editor.Restore(_undo.Pop());
            return true;
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            var editor = new TextEditor();
            var history = new EditorHistory();

            editor.SetState("Первая версия", 13);
            history.Backup(editor);
            editor.SetState("Вторая версия", 6);

            Console.WriteLine(editor.Describe());
            history.Undo(editor);
            Console.WriteLine(editor.Describe());
        }
    }
}

Открыть Memento.cs отдельно Скачать Memento.cs