Комментарии 44
А потом выясняется, что это одна из вещей, о которых надо было позаботиться заранее, причём при наличии умений это лишь минимально увеличивает стоимость всей реализации.
А самая, по-моему, частая фича из числа тех, что часто оставляют на «прикрутим потом», а потом тяжко раскаиваются — распределение прав доступа :-)
Другой случай — есть железо, которое работает в GSM сетях по CSD или GPRS, шлёт пакеты длинной до 255 байт. И не в постоянном режиме, а по запросу с верхнего уровня или спорадически. Опять-же ВНЕЗАПНО руководству захотелось 3G. Типа это модно, современно и быстрее. Доказать, что для нашего трафика никакой разницы нет не удалось. Пришлось искать подходящий нам модем с 3G. Само-собой pin-to-pin совместимых не нашлось, соответственно переразводка платы, доработка напильником прошивки, испытания (в том числе климатические) и прочие радости.
Тут как-раз такая ситуация, что переносное устройство размером со спичечный коробок по дизайну запитывают от БелАЗовского аккумулятора, а потом, спохватившись, добавили повышающую схему для питания от автомобильного аккумулятора. А подобные внезапные хотелки — обычный форс-мажор, такой же, как и переход госструктур на никсы. Этого нельзя было предугадать,
Что касается ваших примеров, то тут косяк главного инженера, ведущего проект. Все излишние хотелки он должен отсекать, это его работа.
Но это третий метод же! «когда при наборе лишь одного символа сохранялся весь документ». А если сохранять все зависимые части, то есть от позиции редактирования до конца документа, то восстановление потребует-таки некоторых пересчётов, как минимум, перерисовки.
Я понимаю, что пример несколько оторванный от реальности, но…
Перерисовка к Undo/Redo вообще никакого отношения иметь не должна.
Вообще, по моему ничтожному мнению, Operation-oriented метод представляет большую гибкость, нежели Value-oriented, так как в рамках операции можно сохранить состояние объекта вместе со всеми зависимостями. И даже более того, можно добавить чек-поинты и пересчёт от них, если чуть-чуть выбраться из коробки. Как это сделать в Value-oriented, я с ходу сказать не имею.
А теперь забудьте об этом методе и более не вспоминайте, ибо это уже не Undo/Redo, а бэкапы.
Назначение хранитель(memento) создавать снимки(snapshot) состояния. Снимок может быть полный или частичный, зависит от требований и сложности предметной области.
А если делать value-oriented через версионирование(как в базах данных с их MVCC), то мы бесплатно получаем full snapshot. А для уменьшения потребления оперативной памяти, можно привлечь хранение данных на диске.
Как то слишком категорично.
А вы мерили производительность, или делаете предположение? Есть практика документ представлять в виде дерева, что может минимизировать объем изменений. А несколько килобайт скидывается/читается очень быстро, т.к. в дисковой подсистеме ОС есть кеш, и фактически работа происходит с оперативной памятью.
Если мы говорит о редакторе документов, то кроме проблемы сохранения изменений, есть проблема отрисовки(рендеринг) с учетов шрифтов, стилей, правил переноса слов и т.д. Вот где нужна быстрота.
Плюсы snapshot-а — объем кодирования не зависит от количества операций. Snapshot помогает сделать undo/redo сразу для всего приложения, а затем в своем темпе добавлять undo-redo-команды для часто встречающихся операций.
Всё-таки в книге GoF паттерн Memento описан лишь как вспомогательное средство в ситуации, когда последовательное применение do и undo не приводит к в точности исходному результату, как, например, при сдвижке объектов на диаграмме (картинка из книжки GoF):
На основе того, что приходилось делать мне, мне представляется, что попытка использовать исключительно Memento для undo приведёт к неудаче. Возможно, неслучайно «Qt такого варианта не предоставил» (но я не специалист по Qt, я по Java-части). Ну а делание снэпшотов всего состояния — это вообще ни в какие ворота, я бы даже всерьёз не стал рассматривать.
Так что может ли возникнуть ситуация, в которой годится что-нибудь ещё, кроме «Command как основное средство + Memento по необходимости» — я не знаю. Не уверен.
При реализации паттернов с моделью, удобней реализовывать value-oriented подход на уровне модели, а остальной код покрывать транзакциями, отделяющими разные логические операции.
Мой личный опыт показывает, что это подход требует реализовать ну, пожалуй, x1.1 логики. И логика undo/redo настолько взаимоувязана, что никакой проблемы с расширением поля для ошибок нет. Потому что, например, do для вставки — это undo для удаления. В комментарии выше ссылка на мою статью, смотрите, например, как устроен там класс SetCellValue.
Если на объекте висят подчинённые объекты и зависимости, то на практике у вас либо удаление зависимости — это тоже отменяемая операция, и в составе макрокоманды при отмене все зависимости восстановятся сами собой (снова отсылаю к своей статье!), либо же всё удалённое дерево объектов будет храниться в команде — реализуя тот самый паттерн Memento.
Под логикой я имел ввиду бизнес логику, то есть непосредственно сам код выполняющий действия, а не весь инфраструктурный код проекта.
Если взять код из вашего проекта, указанного в предыдущей статье, например этот, то можно заметить, что сами описатели команд достаточно большие, а вот логика в них в основном занимает 1-3 строки. И столько же, а иногда и больше требуется для описания логики undo. Для меня это x2.
В моём моём проекте мне удалось отделить саму логику работы приложения от инфраструктурного кода, и лишь обойтись простыми декларациями с помощью атрибутов. И в этот момент количество дополнительного кода засияло в полной мере.
Что касается багов, то они есть всегда. Да, если вся логика занимает пару строк, ошибиться сложнее, но если проект большой или требуются нетривиальные действия, то количество кода начинает расти, а вместе с ним и количество ошибок. И на всё это ещё и юнит тесты написать нужно.
Но всё же основная проблема не в x2 кода, а в том что фича размазана по всему проекту. И в то что при тестировании, для проверки единственной фичи, нужно в 2 раза больше больше всех действий сделать. А при любой пропущеной ошибке в одной из функций отката, для пользователя ломается целиком вся фича undo.
Случаи бывают очень разные. Но автор статьи пишет о Command и Memento как о равноправных методах реализации, а я тут в комментариях пытаюсь отстоять, что предпочтительным подходом всегда является Command как элемент стека Undo + Memento внутри команды во вспомогательных случаях. Лично Вы делали стек Undo на Memento или на Сommand?
Я согласен с тем, что «инфраструктурный» код для undo на базе Command получается довольно громоздким. И всё же я не согласен насчёт того, что код бизнес-логики удваивается… раз уж стали смотреть мой исходник, давайте посмотрим, увеличивается ли вдвое код бизнес-логики:
Класс SetValue:
public void execute() {
changeVal(); //там хоть 2, хоть 200 строк: используем ДВАЖДЫ
}
public void undo() {
changeVal(); //видите? это тот же самый метод
}
Класс Insert:
public void execute() {
internalInsert(map, num); //да будь внутри хоть 2000 строк: мы его используем ДВАЖДЫ
}
public void undo() {
internalDelete(map, num);
}
Класс Delete:
public void execute() {
internalDelete(map, num);
}
public void undo() {
internalInsert(map, num);
map.put(num, deleted); //в переменной deleted команды хранилось то, что было удалено! Если угодно, это такой квази-Memento!
}
Т.к. проект на wpf и соответственно mvvm, то каждое отдельное действие из UI приходят в виде одной команды. Соответственно на вызов каждой команды автоматически открывается и закрытие транзакции.
Сама реализация Undo/Redo находится на уровне модели (что то вроде ORM), которая умеет делать undo и redo. А View автоматически обновляется когда в в модели что то меняется. Таким образом, когда нужна дополнительная фича, достаточно реализовать только её, undo работает автоматом.
Что касается кода вашего примера, на мой взгляд у вас просто смешан инфраструктурный код и код логики, поэтому кажется что всё это логика. Если вы вынесети работу с таблицей в отдельный класс, то окажется что вся ваша бизнес логика представлена вызовом 1-2 методов и аналогичного количества методов для undo. И если считать кодом логики именно указанное вами, то ясно видно что Undo даже больше чем основной логики.
Что касается changeVal — то вы в эту функицю добавили ещё и сохранение предыдущего значения, хотя по идее это именно логика поддержания undo.
Так же хотелось бы заметить ещё один важный момент. Из-за того что вы реализуете undo прямо в команде, у вас каждая команда содержит стейт (который нужен только для undo, то есть по сути его то же к коду undo можно отнести). В моём же случае все команды не содержат стейта и являются статическими, лишь принимая аргументы и контекст снаружи. Из контекста можно получить например выделенные объекты, если нужно работать с ними.
[CmdExecute( CinematicCmd.SetStartTransform )]
public void SetStartTransformCmd( object parameters )
{
var objs = Model.Objects
.Where( x => x.Type.IsGroup() && !x.Type.IsRoot() )
.Select( x => x.GetPropertyObject<ITransformObject>() )
.Where( x => x != null );
var origin = model.OriginPosition.ToMath();
foreach ( var q in objs )
{
q.SetPosition( origin );
q.SetRotation( Quat.Identity() );
q.SetScale( Vec3.One() );
}
}
В то же время для небольших проектов, для которых написание/интегрирование системы управления ресурсами было бы достаточно затратным, паттерн Сommand в этом понимании будет приоритетным.
Хотя Undo к некоторым командам при использовании паттерна Сommand нельзя реализовать вовсе. Например, удаление лишнего в дереве/графе объектов, обновление/отмена обновления/повторное обновление данных из сети и т.д.
Не поймите меня превратно, но у вас те же команды. То, что команда не хранит своё состояние и называется транзакцией — не меняет сути. Чуть сильнее разнесены MVC, иногда это правильно, иногда — нет, но, по сути, вы выполняете действия ВНУТРИ транзакции, а не ВНЕ неё. V-o подход как-раз и заключается в том, что вы в рамках транзакции производите только присваивания.
Автор статьи описывал применение паттерна Команда именно для реализации undo/redo и сравнивал с применением паттерна Хранитель этом контексте. В моём случае я использую второе, а вы первое. И у того и у другого есть свои плюсы, всё зависит от задачи.
Огромная благодарность за пример с Qt QUndoStack
— получается можно отменять что угодно, не только в документах. Попробуем применить к своему случаю.
Undo и Redo — анализ и реализации