Шаблон проектирования для механизма отмены


117

Я пишу инструмент структурного моделирования для приложения гражданского строительства. У меня есть один огромный класс модели, представляющий все здание, которое включает коллекции узлов, линейных элементов, нагрузок и т. Д., Которые также являются настраиваемыми классами.

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

Я могу представить, как бы вы выполняли простые команды, изменяющие свойства объекта и т. Д. Но как насчет сложных команд? Например, вставка новых узловых объектов в модель и добавление некоторых линейных объектов, которые сохраняют ссылки на новые узлы.

Как бы это реализовать?


Если я добавлю комментарий «Undo Algorthim», я смогу выполнить поиск «Undo Algorithm» и найти это? Это то, что я искал, и я нашел что-то закрытое как дубликат.
Питер Тернер

Привет, я также хочу развить отмену / повтор в приложении, которое мы разрабатываем. Мы используем фреймворк QT4, и нам нужно иметь много сложных действий отмены / повтора ... Мне было интересно, удалось ли вам использовать Command-Pattern?
Ашика Уманга Умагилия

2
@umanga: Это сработало, но это было непросто. Самым сложным было отслеживание ссылок. Например, когда объект Frame удаляется, его дочерние объекты: узлы, нагрузки, действующие на него, и многие другие назначения пользователей необходимо было сохранить, чтобы их можно было повторно вставить после отмены. Но некоторые из этих дочерних объектов использовались совместно с другими объектами, и логика отмены / возврата стала довольно сложной. Если бы модель не была такой большой, я бы сохранил подход на память; это намного проще реализовать.
Озгур Озцитак

это забавная проблема для работы, подумайте о том, как это делают репозитории исходного кода, например svn (они сохраняют различия между коммитами).
Alex

Ответы:


88

В большинстве примеров, которые я видел, используется вариант Command-Pattern . Каждое отменяемое действие пользователя получает свой собственный экземпляр команды со всей информацией для выполнения действия и его отката. Затем вы можете вести список всех выполненных команд и откатывать их одну за другой.


4
По сути, так работает механизм отмены в Cocoa, NSUndoManager.
amrox

33

Я думаю, что и сувенир, и команда непрактичны, когда вы имеете дело с моделью того размера и объема, которые подразумевает OP. Они будут работать, но потребуется много работы по поддержанию и расширению.

Я думаю, что для этого типа проблемы вам необходимо встроить поддержку вашей модели данных, чтобы поддерживать дифференциальные контрольные точки для каждого объекта, участвующего в модели. Я сделал это однажды, и это сработало очень гладко. Самое важное, что вам нужно сделать, - это избегать прямого использования указателей или ссылок в модели.

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

Реализовать отмену / повтор просто: выполните свое действие и установите новую контрольную точку; откатить все версии объекта к предыдущей контрольной точке.

Это требует некоторой дисциплины в коде, но имеет много преимуществ: вам не нужны глубокие копии, поскольку вы делаете дифференциальное хранение состояния модели; вы можете ограничить объем памяти, который хотите использовать (что очень важно для таких вещей, как модели САПР), либо количеством повторов, либо использованной памятью; очень масштабируемые и простые в обслуживании для функций, которые работают с моделью, поскольку им не нужно ничего делать для реализации отмены / повтора.


1
Если вы используете базу данных (например, sqlite) в качестве формата файла, это может быть почти автоматическим
Мартин Беккет,

4
Если вы увеличите это, отслеживая зависимости, вносимые изменениями в модель, тогда у вас потенциально может быть система дерева отмены (т.е. если я изменю ширину фермы, а затем поработаю над отдельным компонентом, я могу вернуться и отменить ферма меняется без потери остального). Пользовательский интерфейс для этого может быть немного громоздким, но он будет намного более мощным, чем традиционная линейная отмена.
Sumudu Fernando

Не могли бы вы подробнее объяснить идею этого идентификатора и указателей? Конечно, адрес указателя / памяти работает так же хорошо, как и id?
Paulm

@paulm: по сути фактические данные индексируются по (id, version). Указатели относятся к определенной версии объекта, но вы хотите указать текущее состояние объекта, каким бы оно ни было, поэтому вы хотите адресовать его по идентификатору, а не по (id, версия). Вы можете реструктурировать его так, чтобы вы сохраняли указатель на таблицу (версия => данные) и каждый раз просто выбирали самую последнюю, но это имеет тенденцию навредить локальности, когда вы сохраняете данные, немного запутывает проблемы и усложняет выполнять какие-то общие запросы, поэтому обычно это делается не так.
Крис Морган,

17

Если вы говорите о GoF, шаблон Memento специально предназначен для отмены.


7
Не совсем, это касается его первоначального подхода. Он просит альтернативного подхода. Первоначально хранится полное состояние для каждого шага, а во втором - только "различия".
Андрей Рыня

15

Как утверждали другие, шаблон команды - очень мощный метод реализации отмены / возврата. Но есть важное преимущество, которое я хотел бы отметить в шаблоне команд.

При реализации отмены / повтора с использованием шаблона команд вы можете избежать большого количества дублированного кода, абстрагируя (в определенной степени) операции, выполняемые с данными, и используя эти операции в системе отмены / повтора. Например, в текстовом редакторе вырезание и вставка являются дополнительными командами (помимо управления буфером обмена). Другими словами, операция отмены для вырезания - это вставка, а операция отмены для вставки - это вырезка. Это относится к гораздо более простым операциям, таким как ввод и удаление текста.

Ключевым моментом здесь является то, что вы можете использовать свою систему отмены / повтора в качестве основной системы команд для вашего редактора. Вместо того, чтобы писать систему, такую ​​как «создать объект отмены, изменить документ», вы можете «создать объект отмены, выполнить операцию повтора для объекта отмены, чтобы изменить документ».

Сейчас, по общему признанию, многие люди думают про себя: «Ну да, разве не является частью шаблона команды?» Да, но я видел слишком много систем команд, которые имеют два набора команд: один для немедленных операций, а другой - для отмены / повтора. Я не говорю, что не будет команд, специфичных для немедленных операций и отмены / повтора, но уменьшение дублирования сделает код более удобным в сопровождении.


1
Я никогда не думал о том, pasteкак cut^ -1.
Ленар Хойт

8

Вы можете обратиться к коду Paint.NET для их отмены - у них действительно хорошая система отмены. Это, вероятно, немного проще, чем то, что вам нужно, но может дать вам некоторые идеи и рекомендации.

-Адам


4
Фактически, код Paint.NET больше не доступен, но вы можете получить разветвленный код. Google.com/p/paint-mono
Игорь Брейц

7

Это может быть тот случай, когда применим CSLA . Он был разработан для обеспечения комплексной поддержки отмены для объектов в приложениях Windows Forms.


6

Я успешно реализовал сложные системы отмены, используя шаблон Memento - очень просто, и преимущество также заключается в естественном предоставлении инфраструктуры Redo. Более тонкое преимущество заключается в том, что совокупные действия также могут содержаться в одной отмене.

Короче говоря, у вас есть две стопки памятных предметов. Один для отмены, другой для возврата. Каждая операция создает новый сувенир, который в идеале должен представлять собой несколько вызовов для изменения состояния вашей модели, документа (или чего-то еще). Он добавляется в стек отмены. Когда вы выполняете операцию отмены, помимо выполнения действия Undo для объекта Memento, чтобы снова изменить модель, вы также выталкиваете объект из стека Undo и помещаете его прямо в стек Redo.

Как реализован метод изменения состояния вашего документа, полностью зависит от вашей реализации. Если вы можете просто выполнить вызов API (например, ChangeColour (r, g, b)), то перед ним укажите запрос, чтобы получить и сохранить соответствующее состояние. Но шаблон также будет поддерживать создание глубоких копий, снимков памяти, создание временных файлов и т. Д. - все зависит от вас, поскольку это просто реализация виртуального метода.

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

Многие системы отмены находятся только в памяти, но я думаю, вы можете сохранить стек отмены, если хотите.


5

Только что прочитал о шаблоне команд в моей книге по гибкой разработке - может быть, у него есть потенциал?

Вы можете заставить каждую команду реализовывать командный интерфейс (который имеет метод Execute ()). Если вы хотите отменить, вы можете добавить метод отмены.

подробнее здесь


4

Я поддерживаю Мендельта Зибенга в том, что вам следует использовать командный шаблон. Вы использовали шаблон Memento Pattern, который со временем может стать очень расточительным.

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

Я бы посоветовал вам проверить, есть ли фреймворк, который уже создал модель для отмены на выбранном вами языке программирования / фреймворке. Придумывать новое приятно, но лучше взять что-то уже написанное, отлаженное и протестированное на реальных сценариях. Было бы полезно, если бы вы добавили то, на чем пишете, чтобы люди могли рекомендовать знакомые им фреймворки.


3

Проект Codeplex :

Это простой фреймворк для добавления функциональности Undo / Redo в ваши приложения, основанный на классическом шаблоне проектирования Command. Он поддерживает действия слияния, вложенные транзакции, отложенное выполнение (выполнение при фиксации транзакции верхнего уровня) и возможную нелинейную историю отмены (где вы можете выбрать несколько действий для повторения).


2

В большинстве примеров, которые я читал, это делается с использованием либо команды, либо шаблона сувенира. Но вы можете сделать это и без шаблонов проектирования с помощью простой структуры deque .


Что бы вы положили в дек?

В моем случае я помещаю текущее состояние операций, для которых мне нужна функция отмены / повтора. Имея две очереди (отменить / повторить), я делаю отмену в очереди отмены (выталкивает первый элемент) и вставляю его в очередь отмены. Если количество элементов в очереди превышает желаемый размер, я вытаскиваю элемент хвоста.
Патрик Свенссон,

2
То , что вы описали фактически IS шаблон дизайна :). Проблема с этим подходом заключается в том, что ваше состояние занимает много памяти - тогда сохранение нескольких десятков версий состояния становится непрактичным или даже невозможным.
Игорь Брейц

Или вы можете сохранить пару замыканий, представляющих нормальную операцию и операцию отмены.
Xwtek 03

2

Умный способ обработки отмены, который сделал бы ваше программное обеспечение также подходящим для многопользовательской совместной работы, - это внедрение операционной трансформации. структуры данных.

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



1

Мы повторно использовали загрузку файла и код сериализации для «объектов» в удобной форме для сохранения и восстановления всего состояния объекта. Мы помещаем эти сериализованные объекты в стек отмены - вместе с некоторой информацией о том, какая операция была выполнена, и намеками на отмену этой операции, если из сериализованных данных извлечено недостаточно информации. Отмена и возврат часто просто заменяют один объект другим (теоретически).

Было МНОЖЕСТВО ошибок из-за указателей (C ++) на объекты, которые никогда не исправлялись, поскольку вы выполняете некоторые странные последовательности отмены повторения (те места, которые не обновлялись для более безопасных «идентификаторов», учитывающих отмену). Баги в этой области часто ... ммм ... интересно.

Некоторые операции могут быть особыми случаями использования скорости / ресурсов - например, определение размеров, перемещение предметов.

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


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

Каким образом? Этот подход продолжает работать без изменений, поскольку к каждому объекту добавляются новые «вещи». Производительность может быть проблемой, поскольку сериализованная форма объектов увеличивается в размере, но это не было большой проблемой. Система находится в постоянном развитии более 20 лет и используется тысячами пользователей.
Aardvark,

1

Мне пришлось сделать это при написании решателя для игры-головоломки с прыжками от колышка. Я делал каждое движение командным объектом, который содержал достаточно информации, чтобы его можно было сделать или отменить. В моем случае это было так же просто, как сохранение начальной позиции и направления каждого движения. Затем я сохранил все эти объекты в стеке, чтобы программа могла легко отменить столько ходов, сколько нужно, при возврате.


1

Вы можете попробовать готовую реализацию паттерна Undo / Redo в PostSharp. https://www.postsharp.net/model/undo-redo

Он позволяет добавлять в приложение функции отмены / повтора действий без самостоятельной реализации шаблона. Он использует шаблон Recordable для отслеживания изменений в вашей модели и работает с шаблоном INotifyPropertyChanged, который также реализован в PostSharp.

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


0

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


0

В первом разделе паттернов проектирования (GoF, 1994) описан вариант использования отмены / повтора в качестве шаблона проектирования.


0

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

Используйте постоянные структуры данных и придерживайтесь списка ссылок на старое состояние . (Но это действительно работает только в том случае, если операции все данные в вашем классе состояния неизменяемы, и все операции с ним возвращают новую версию --- но новая версия не должна быть глубокой копией, просто замените копию измененных частей -он-запись».)


0

Я обнаружил, что здесь очень полезен шаблон Command. Вместо реализации нескольких обратных команд я использую откат с отложенным выполнением на втором экземпляре моего API.

Этот подход кажется разумным, если вам нужны небольшие усилия по реализации и простота обслуживания (и вы можете позволить себе дополнительную память для второго экземпляра).

См. Пример здесь: https://github.com/thilo20/Undo/


-1

Я не знаю, будет ли это полезно для вас, но когда мне пришлось сделать что-то подобное в одном из моих проектов, я в конечном итоге загрузил UndoEngine с http://www.undomadeeasy.com - замечательный движок и меня действительно не особо заботило, что находится под капотом - это просто работало.


Пожалуйста, оставляйте свои комментарии в качестве ответа только в том случае, если вы уверены, что сможете предложить решения! В противном случае лучше разместить его как комментарий под вопросом! (если это не позволяет сделать это сейчас! пожалуйста, подождите, пока вы не заработаете хорошую репутацию)
InfantPro'Aravind '11

-1

На мой взгляд, UNDO / REDO можно реализовать двумя способами. 1. Командный уровень (называемый командным уровнем Отменить / Вернуть) 2. Уровень документа (называемый глобальным Отменить / Вернуть)

Командный уровень: как отмечается во многих ответах, это эффективно достигается с помощью шаблона Memento. Если команда также поддерживает ведение журнала действия, повторение легко поддерживается.

Ограничение: как только область действия команды исчерпана, отменить / повторить невозможно, что приводит к отмене / повторному выполнению на уровне документа (глобальном).

Я предполагаю, что ваш случай вписался бы в глобальную отмену / повтор, поскольку он подходит для модели, которая требует много места в памяти. Кроме того, это также подходит для выборочной отмены / повтора. Есть два примитивных типа

  1. Отменить / повторить всю память
  2. Уровень объекта Отменить Повторить

В режиме «Отменить / повторить всю память» вся память рассматривается как связанные данные (такие как дерево, список или граф), а память управляется приложением, а не ОС. Таким образом, операторы new и delete, если в C ++ перегружены, чтобы содержать более конкретные структуры для эффективной реализации таких операций, как. Если какой-либо узел изменен, b. хранение и очистка данных и т. д. Принцип его работы заключается в том, чтобы скопировать всю память (при условии, что выделение памяти уже оптимизировано и управляется приложением с использованием передовых алгоритмов) и сохранить ее в стеке. Если запрашивается копия памяти, древовидная структура копируется в зависимости от необходимости иметь мелкую или глубокую копию. Глубокая копия делается только для той переменной, которая изменена. Поскольку каждая переменная выделяется с использованием настраиваемого распределения, последнее слово остается за приложением, когда его удалять, если это необходимо. Вещи становятся очень интересными, если нам нужно разделить Undo / Redo, когда случается так, что нам нужно программно выборочно отменить / Redo набор операций. В этом случае только эти новые переменные, или удаленные переменные, или измененные переменные получают флаг, так что Undo / Redo отменяет / повторяет только эту память. Вещи становятся еще более интересными, если нам нужно выполнить частичную Undo / Redo внутри объекта. В таком случае используется более новая идея «паттерна посетителя». Это называется «Отменить / повторить на уровне объекта». или удаленным переменным, или измененным переменным присваивается флаг, так что Undo / Redo отменяет / повторяет только эту память. Вещи становятся еще более интересными, если нам нужно выполнить частичную Undo / Redo внутри объекта. В таком случае используется более новая идея «паттерна посетителя». Это называется «Отменить / повторить на уровне объекта». или удаленным переменным, или измененным переменным присваивается флаг, так что Undo / Redo отменяет / повторяет только эту память. Вещи становятся еще более интересными, если нам нужно выполнить частичную Undo / Redo внутри объекта. В таком случае используется более новая идея «паттерна посетителя». Это называется «Отменить / повторить на уровне объекта».

  1. Отменить / повторить на уровне объекта: когда вызывается уведомление об отмене / повторении, каждый объект реализует операцию потоковой передачи, при которой стример получает от объекта старые / новые данные, которые запрограммированы. Данные, которые не могут быть нарушены, остаются нетронутыми. Каждый объект получает стример в качестве аргумента, и внутри вызова UNDo / Redo он передает / отменяет поток данных объекта.

И 1, и 2 могут иметь такие методы, как 1. BeforeUndo () 2. AfterUndo () 3. BeforeRedo () 4. AfterRedo (). Эти методы должны быть опубликованы в основной команде Undo / redo (а не в контекстной команде), чтобы все объекты также реализовали эти методы для получения определенного действия.

Хорошая стратегия - создать гибрид 1 и 2. Прелесть в том, что эти методы (1 и 2) сами используют шаблоны команд.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.