handbookНаписать

patterns/behavioral/memento/README.md · тема · 4 языка · 19 тестов

+ практика: черновики анкеты

Снимок (Memento) ​

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

Проблема ​

В графическом редакторе есть кнопка «Отменить». Проще всего сделать её снаружи: перед каждой операцией история копирует поля холста (штрихи, текущий цвет) к себе, а при отмене записывает их обратно. Для этого холст должен открыть все поля на чтение и запись. Теперь история знает устройство холста: добавили слой или масштаб — правим историю, забыли — отмена восстанавливает холст наполовину. А раз поля открыты, их может поменять кто угодно, не только отмена.

Решение ​

Холст сам упаковывает своё состояние в снимок CanvasSnapshot и сам из него восстанавливается. Снимок неизменяем и непрозрачен: снаружи видна только подпись, состояние прочитать может только холст. История хранит снимки и в нужный момент отдаёт их холсту обратно, ни разу не заглянув внутрь.

Три роли: создатель (Canvas) делает и принимает снимки, снимок хранит состояние, опекун (History) решает, когда снимать и к какому снимку вернуться. Главный вопрос приёма — как закрыть снимок от всех, кроме создателя, и здесь у каждого языка свой ответ.

Структура ​

creates, reads

keeps

Canvas

-strokes: string[]

-color: string

+setColor(color)

+draw(shape)

+save(label) : CanvasSnapshot

+restore(snapshot)

CanvasSnapshot

+label: string

-strokes: string[]

-color: string

History

-snapshots: CanvasSnapshot[]

+backup(label)

+undo() : string

+labels() : string[]

Canvas — создатель. save() копирует штрихи в снимок, restore() копирует их обратно, а не берёт ссылку: иначе следующий штрих после отмены дописался бы прямо в снимок, и второй откат к нему вернул бы уже испорченное состояние. Геттеры strokes() и color() нужны тестам и интерфейсу, истории они не нужны.

Как это собирается ​

Canvas и History создаёт окно редактора при открытии рисунка, и история получает холст в конструктор. Когда снимать, решает код приложения, а не холст и не история: в примере правило "снимок перед каждым штрихом", поэтому backup зовёт обработчик кнопки рисования, а смена цвета снимка не делает. Подпись снимка (before circle) - то, что потом покажет пункт меню "Отменить".

Ctrl+Z зовёт undo: история отдаёт холсту последний снимок, холст сам раскладывает его обратно в штрихи и цвет. Когда снимков больше нет, история бросает ошибку "nothing to undo", и обработчик просто сообщает об этом пользователю: в приложении в этот момент кнопка отмены становится неактивной.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

History — опекун-стек: backup() просит у холста снимок и кладёт его наверх, undo() снимает верхний и отдаёт холсту. Сам он видит только подписи (labels()), этого хватает, чтобы показать пользователю список «Отменить: круг».

ЯзыкКак закрыт снимок
PHPУзкий интерфейс Snapshot для опекуна и широкий final readonly class CanvasSnapshot для холста
GoНеэкспортируемые поля: из другого пакета снимок не прочитать и не подделать, внутри пакета memento холст читает их напрямую
PythonСоглашение: поля _strokes и _color с подчёркиванием, frozen=True от случайной записи, кортеж вместо списка
TypeScriptНастоящие приватные поля #; модуль открывает их себе через static-блок и неэкспортируемую функцию readState

Запуск: make test T=patterns/behavioral/memento

Плюсы и минусы ​

Тесты одинаковы во всех языках. Отмена возвращает и штрихи, и цвет, хотя история ни про то, ни про другое не знает. Две отмены подряд проходят стек до пустого холста. Опекун видит только подписи. Отмена на пустой истории — ошибка, а не молчаливое ничего.

  • ✅ Инкапсуляция цела: устройство холста знает только холст, история работает с любым состоянием.
  • ✅ Новое поле холста попадает в отмену правкой save()/restore() в одном месте.
  • ✅ Снимок неизменяем, его можно хранить, передавать и восстанавливать сколько угодно раз.
  • ❌ Память: каждый снимок — полная копия состояния. Для большого документа это дорого, нужен лимит истории или хранение разниц.
  • ❌ Языки без «дружественного» доступа заставляют выбирать между строгостью и простотой (см. отличия).

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

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

Отличия между языками ​

  • PHP — видимость в PHP на уровне класса, дружественных классов нет. Поэтому использован вариант авторов паттерна: узкий интерфейс Snapshot (только label()) для опекуна и широкий класс CanvasSnapshot для создателя. Опекун типизирован узким интерфейсом и до штрихов не дотягивается, а readonly не даёт их поменять. Обойти это можно привязанным замыканием (Closure::bind), но такой трюк прячет чтение от PHPStan. Массивы PHP копируются по значению, так что отдельно копировать штрихи не нужно.
  • Go — граница видимости — пакет, и это ровно «дружественный» доступ: создатель и снимок в одном пакете, опекун из любого другого видит только Label(). Срезы делят массив, поэтому Save и Restore клонируют их через slices.Clone.
  • Python — приватности нет, есть соглашение: подчёркнутые поля не трогают снаружи, линтеры это подсвечивают. Если нужна настоящая закрытость, состояние можно спрятать в замыкание: save() возвращает объект с подписью и функцией, которая восстанавливает холст из захваченных значений.
  • TypeScript — поля # закрыты на уровне движка, даже от другого класса в том же файле. Мостик — static-блок, который кладёт читатель в неэкспортируемую переменную модуля: читать снимок может только код этого модуля. Модификатор private TS был бы проверкой только компилятора.

Практика ​

Учебная история хранит снимки без счёта и только в памяти. В приложении снимков много, они сохраняются по таймеру, переживают перезагрузку страницы и должны отличать свой формат от старого. Практика: черновики анкеты показывает автосохранение не чаще раза в N секунд на подменяемых часах, именованные точки и откат к ним, лимит истории и снимок, сериализованный в строку с версией формата.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS