Также известен как Хранитель, Memento

Снимок

Суть паттерна

Позволяет делать снимок состояния объекта, не раскрывая подробностей его реализации. Позже можно будет восстановить прошлое состояние объекта, используя этот снимок.

Проблема

Нужно восстанавливать объекты к их прежнему состоянию (т.е. иметь операцию отмены). Причём отмену хочется реализовать так, чтобы не раскрывать внутреннего представления объектов системы.

Другими словами, вам не хочется делать все поля классов публичными ради того, чтобы получить возможность копировать их данные.

Решение

Если снятие снимка «извне» объекта нарушит инкапсуляцию, то почему бы не снять его «изнутри»?

Объект, владеющий состоянием, должен иметь два метода:

  • сделатьСнимок(): будет создавать специальный объект-снимок и заполнять его текущими значениями своих публичных и приватных полей.
  • восстановитьСостояние(х: Снимок): должен скопировать значения полей из поданного объекта-снимка.

Теперь, перед изменением состояния объекта, Клиент сможет сначала получить его снимок и сохранить его в истории. Затем, если нужно вернуть прежнее состояние, Клиент передаст снимок обратно в объект.

Как видите, чтобы сделать операции отмены и повтора в вашем приложении, достаточно иметь стек из Команд, и стек из Снимков.

Структура

Классическая реализация

Схема структуры классов паттерна Снимок (Хранитель)

Снимки с повышенной защитой

Существует альтернативная реализация паттерна Снимок, с ещё более жёстким контролем доступа к данным объекта-снимка.

Снимок с повышенной защитой
  1. Создатель делает снимки своего состояния по запросу, а также воспроизводит прошлое состояние, если подать в него готовый снимок.

  2. Снимок простой объект данных, содержащий состояние Создателя. Объект снимка должен быть неизменяемым. В идеале, Создатель передаёт данные в снимок за один раз через конструктор.

  3. Клиент должен знать когда и зачем делать снимок Создателя, а также когда его нужно восстановить.

  4. Зачастую клиент содержит целый стек из объектов-снимков. Если понадобится сделать отмену, он возьмёт последний снимок из стека.

  5. В Создателе метод восстановления нужно заменить прямыми сеттерами состояния.

  6. Снимок теряет публичный геттер состояния.

    Взамен снимок получит публичный метод восстановления, из которого он и передаст в исходный объект своё приватное состояние.

  7. В этой реализации код Клиента чуточку проще, так как для начала восстановления теперь нужен только объект снимка.

Псевдокод

// Класс создателя должен иметь специальный метод, который сохраняет состояние
// создателя в новом объекте-снимке.
class Editor is
    private field text: string
    private field cursorX, cursorY, selectionWidth

    method setText(text) is
        this.text = text

    method setCursor(x, y) is
        this.cursorX = cursorX
        this.cursorY = cursorY

    method selectionWidth(width) is
        this.selectionWidth = width

    method saveState():EditorState is
        // Снимок — неизменяемый объект, поэтому Создатель передаёт все своё
        // состояние через параметры конструктора.
        return new EditorState(this, text, cursorX, cursorY, selectionWidth)

// Снимок хранит прошлое состояние редактора.
class EditorState is
    private field editor: Editor
    private field text: string
    private field cursorX, cursorY, selectionWidth

    constructor EditorState(editor, text, cursorX, cursorY, selectionWidth) is
        this.editor = editor
        this.text = text
        this.cursorX = cursorX
        this.cursorY = cursorY
        this.selectionWidth = selectionWidth

    // В нужный момент, владелец снимка может восстановить состояние редактора.
    method restore() is
        editor.setText(text)
        editor.setCursor(cursorX, cursorY)
        editor.selectionWidth(selectionWidth)

// Клиентом может выступать класс команд (см. паттерн Команда). В этом случае,
// команда сохраняет снимок получателя перед тем, как выполнить действие. А при
// отмене, возвращает получателя в предыдущее состояние.
class Command is
    field backup: EditorState

    method backup() is
        backup = editor.saveState()

    method undo() is
        if (backup != null)
            backup.restore()
    // ...

Применимость

Вам нужно сохранять мгновенный снимок состояния объекта (или его части), чтобы впоследствии объект можно было восстановить в том же состоянии.

Паттерн Снимок позволяет делать любое количество снимков объекта и хранить их независимо от объекта, с которого делают снимок. Снимки часто используют не только для реализации операции отмены, но и для транзакций (когда состояние объекта нужно откатить, если операция не удалась).

Прямое получение состояния объекта раскрывает детали его реализации, нарушая инкапсуляцию.

Паттерн предлагает изготовить снимок самому исходному объекту, так как ему доступны все поля, даже приватные.

Шаги реализации

  1. Определите состояние каких объектов нужно отматывать обратно во времени.

  2. Создайте класс Снимка. Сделайте его неизменяемым.

  3. Добавьте в исходный класс операцию создания снимков. В этой операции исходный объект должен создавать новый объект-снимок и копировать в него значения всех своих полей.

  4. Добавьте в исходный класс операцию восстановления состояния из снимков.

  5. Клиент должен знать о том, когда запрашивать снимки у исходного объекта, где их хранить, и когда восстанавливать.

Преимущества и недостатки

  • Не нарушает инкапсуляции исходного объекта.
  • Упрощает структуру исходного объекта. Ему не нужно хранить историю версий своего состояния.
  • Требует много памяти, если клиенты слишком часто создают снимки.
  • Может повлечь дополнительные издержки памяти, если объекты хранящие историю не освобождают ресурсы, занятые устаревшими снимками.
  • В некоторых языках (например Python, JavaScript) сложно гарантировать, чтобы только исходный объект имел доступ к состоянию снимка.

Отношения с другими паттернами

  • Команду и Снимок часто используют сообща: Команда содержит в себе данные запроса, а Снимок сберегает состояние объекта в определённый момент времени. Для Команды важен полиморфизм, чтобы разные команды можно было подавать одному и тому же получателю. С другой стороны, интерфейс Снимка слишком узок, так как рассчитан на работу с определённым исходным объектом.

  • Команда может делать Снимок состояния объекта перед тем как выполнить действие, чтобы потом это действие можно было отменить.

  • Снимок часто используется вместе с Итератором. Итератор может держать в снимке текущее состояние обхода своей структуры данных.

Реализация в различных языках программирования

Java