Шаблон дизайну для скасування двигуна


117

Я пишу інструмент структурного моделювання для програми цивільного інжинірингу. У мене є один величезний модельний клас, що представляє всю будівлю, що включає колекції вузлів, лінійних елементів, навантажень тощо, які також є спеціальними класами.

Я вже зашифрував механізм скасування, який зберігає глибоку копію після кожної модифікації моделі. Тепер я почав думати, чи можу я кодувати інше. Замість збереження глибоких копій я міг би зберегти список кожної дії модифікатора за допомогою відповідного зворотного модифікатора. Щоб я міг застосувати зворотні модифікатори до поточної моделі для скасування, або модифікатори для повторного скасування.

Я можу собі уявити, як ви виконували б прості команди, що змінюють властивості об'єкта тощо. А як щодо складних команд? Як і вставлення в модель нових об’єктів вузла та додавання деяких лінійних об'єктів, які зберігають посилання на нові вузли.

Як можна було б реалізувати це?


Якщо я додаю коментар "Скасувати алгоритм", це зробить це, щоб я міг шукати "Скасувати алгоритм" і знайти це? Це те, що я шукав, і я знайшов щось закрите як дублікат.
Пітер Тернер

сіно, я також хочу розробити скасувати / повторити в застосуванні, яке ми розробляємо. Ми використовуємо QT4 фреймворк і потрібно мати багато складних дій для скасування / повторення. Мені було цікаво, чи вдалося вам використовувати Command-Pattern?
Ashika Umanga Umagiliya

2
@umanga: Це спрацювало, але це було непросто. Найважче було відслідковувати посилання. Наприклад, коли об’єкт Frame видалено, його дочірні об’єкти: Вузли, Навантаження, що діють на нього, та багато інших призначень користувачів, які потрібно зберегти, щоб повторно вставити їх після скасування. Але деякі з цих дочірніх об'єктів були спільними з іншими об'єктами, і логіка скасування / повтору стала досить складною. Якби модель була не такою великою, я б дотримувався підходу; це набагато простіше здійснити.
Озгур Оззітак

над цією цікавою проблемою працювати, подумайте про те, як це зробити репост вихідного коду, як svn (вони зберігають відмінності між комітами).
Олексій

Відповіді:


88

Більшість прикладів, які я бачив, для цього використовують варіант Command-Pattern . Кожна дія користувача, яка не змінюється, отримує власний екземпляр команди з усією інформацією про виконання дії та повернення її назад. Потім ви можете підтримувати список всіх команд, які були виконані, і ви можете їх повертати одну за одною.


4
Це в основному, як працює двигун для скасування в Cocoa, NSUndoManager, працює.
amrox

33

Я думаю, що і пам'ять, і команда не є практичними, коли ви маєте справу з моделлю розміру та обсягу, яку передбачає ОП. Вони працювали б, але було б багато роботи, щоб підтримувати та розширювати.

Для цього типу проблем, я думаю, вам потрібно створити підтримку вашої моделі даних, щоб підтримувати різні контрольні точки для кожного об'єкта, що бере участь у моделі. Я робив це один раз, і це спрацювало дуже гладко. Найбільше, що вам потрібно зробити, - це уникати прямого використання покажчиків або посилань у моделі.

Кожна посилання на інший об'єкт використовує певний ідентифікатор (наприклад, ціле число). Щоразу, коли потрібен об'єкт, ви шукаєте поточне визначення об’єкта з таблиці. Таблиця містить зв'язаний список для кожного об’єкта, який містить усі попередні версії, а також інформацію щодо того, для якої контрольної точки вони були активними.

Реалізація скасування / повтору проста: зробіть свої дії та встановіть нову контрольну точку; відкат усіх версій об'єкта до попередньої контрольної точки.

Це вимагає певної дисципліни в коді, але має багато переваг: вам не потрібні глибокі копії, оскільки ви робите диференційоване зберігання стану моделі; ви можете обширити об'єм пам'яті, який ви хочете використовувати ( дуже важливо для таких речей, як CAD-моделі), будь-якою кількістю повторень або використовуваної пам'яті; дуже масштабований і малозабезпечений для функцій, які працюють на моделі, оскільки їм не потрібно нічого робити для того, щоб здійснити скасування / повтор.


1
Якщо ви використовуєте базу даних (наприклад, sqlite) в якості формату файлу, це може бути майже автоматичним
Martin Beckett

4
Якщо ви збільшуєте це шляхом відстеження залежностей, внесених змінами в модель, тоді ви потенційно можете мати систему скасування дерева (тобто, якщо я зміню ширину балки, то перейдіть на деяку роботу над окремим компонентом, я можу повернутися і скасувати балка змінюється, не втрачаючи іншого матеріалу). Користувацький інтерфейс для цього може бути трохи непростим, але він буде набагато потужнішим, ніж традиційні лінійні скасування.
Сумуду Фернандо

Чи можете ви пояснити ідею цього ідентифікатора проти покажчиків більше? Звичайно, вказівник / адреса пам'яті працює так само добре, як і id?
паульм

@paulm: фактично фактичні дані індексуються (id, версія). Покажчики посилаються на певну версію об'єкта, але ви прагнете вказати на поточний стан об'єкта, який би це не був, тому ви хочете адресувати його за id, а не за (id, версія). Ви можете її реструктурувати, щоб ви зберігали вказівник на таблицю (version => даних) і щоразу вибирали найновіші, але це, як правило, шкодить місцевості, коли ви зберігаєте дані, каламутніє мало, і робить це складніше робити якісь поширені запити, тож це не так, як це було б нормально.
Кріс Морган

17

Якщо ви говорите про GoF, шаблон Memento конкретно стосується скасування.


7
Насправді це стосується його початкового підходу. Він просить альтернативного підходу. Початкове зберігає повний стан для кожного кроку, тоді як останній зберігає лише "розріз".
Андрій Ронеа

15

Як зазначають інші, командна схема є дуже потужним методом реалізації Undo / Redo. Але є важлива перевага, яку я хотів би зазначити в командному шаблоні.

Реалізуючи скасувати / повторити за допомогою шаблону команд, ви можете уникнути великої кількості дублюваного коду, абстрагувавши (до певної міри) операції, що виконуються над даними, та використовуючи ці операції в системі скасування / повтор. Наприклад, у текстовому редакторі вирізати та вставити - це додаткові команди (окрім управління буфером обміну). Іншими словами, операція відміни для вирізу є вставкою, а операція відміни для вставки вирізається. Це стосується набагато простіших операцій, як введення та видалення тексту.

Ключовим тут є те, що ви можете використовувати вашу систему скасування / повторення як основну командну систему для свого редактора. Замість того, щоб писати систему на зразок "створити скасувати об'єкт, змінити документ", ви можете "створити скасувати об'єкт, виконати повторну операцію на скасувати об'єкт для зміни документа".

Зараз, правда, багато людей думають собі: "Ну так, це не є частиною пункту командної схеми?" Так, але я бачив занадто багато командних систем, які мають два набори команд, один для негайних операцій та інший набір для скасування / повторення. Я не кажу, що не буде команд, специфічних для негайних операцій, і скасувати / повторити, але зменшення дублювання зробить код більш рентабельним.


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

8

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

-Адам


4
Насправді код Paint.NET більше недоступний, але ви можете отримати роздвоєний code.google.com/p/paint-mono
Ігор Брейц

7

Це може бути випадок, коли застосовується CSLA . Він був розроблений для надання складної підтримки для скасування об’єктів у додатках Windows Forms.


6

Я успішно реалізував складні системи скасування за допомогою шаблону Memento - дуже просто і має перевагу, природно, створити рамку Redo. Більш тонка перевага полягає в тому, що сукупні дії можуть міститися і в одному Скасувати.

У двох словах, у вас є два стеки об'єктів. Один для Скасувати, інший для Редо. Кожна операція створює нову пам’ятку, яка в ідеалі буде деякими закликами змінити стан вашої моделі, документа (або будь-якого іншого). Це додається до стеки скасування. Коли ви виконайте операцію скасування, крім виконання дії «Скасувати» на об’єкті «Мементо», щоб знову змінити модель назад, ви також висуньте об'єкт зі стеку «Скасувати» і натисніть його прямо на стек «Повторити».

Як реалізується метод зміни стану документа, повністю залежить від вашої реалізації. Якщо ви можете просто зробити виклик API (наприклад, ChangeColour (r, g, b)), то передуйте йому запит, щоб отримати та зберегти відповідний стан. Але шаблон також підтримуватиме створення глибоких копій, знімків пам'яті, створення темп-файлів тощо - все залежить від вас, оскільки це просто реалізація віртуального методу.

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

Багато систем скасування є лише в пам'яті, але ви можете зберегти скасування стека, якщо бажаєте.


5

Щойно читав про шаблон команд у моїй спритній книзі розвитку - можливо, це має потенціал?

Ви можете мати кожну команду реалізувати командний інтерфейс (у якому є метод Execute ()). Якщо ви хочете скасувати, ви можете додати метод скасування.

Більше інформації тут


4

Я разом із Mendelt Siebenga про те, що вам слід використовувати командний шаблон. Ви використовували зразок - шаблон Memento, який з часом може стати дуже марним.

Оскільки ви працюєте над додатком, що займає велику пам’ять, ви повинні мати змогу вказати, скільки пам'яті дозволено займати двигун для скасування, скільки рівнів скасування збережено або деяке сховище, до якого вони будуть зберігатися. Якщо ви цього не зробите, незабаром ви зіткнетеся з помилками внаслідок того, що машина втратила пам'ять.

Я б радив вам перевірити, чи є рамка, яка вже створила модель для скасування в мові програмування / рамки на ваш вибір. Приємно вигадувати нові речі, але краще взяти щось вже написане, налагоджене і перевірене в реальних сценаріях. Допоможе, якщо ви додасте те, про що пишете, щоб люди могли рекомендувати відомі їм рамки.


3

Проект Codeplex :

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


2

Більшість прикладів, які я читав, роблять це, використовуючи або команду, або шаблон пам'яті. Але ви можете зробити це без шаблонів проектування теж з допомогою простого DEQUE-структури .


Що б ви поставили в деке?

У моєму випадку я помістив поточний стан операцій, для яких хотів скасувати / повторити функціональність. Маючи дві деки (скасувати / повторити), я скасовую їх у черзі відміни (з’являється перший елемент) і вставляю її у повторну декею. Якщо кількість предметів у декевах перевищує бажаний розмір, я спливає предмет хвоста.
Патрік Свенссон

2
Те, що ви описуєте, насправді - це модель дизайну :). Проблема такого підходу полягає в тому, що ваша держава займає багато пам’яті - зберігання декількох десятків версій стану тоді стає непрактичним або навіть неможливим.
Ігор Брейц

Або ви можете зберігати пару закриття, що відображає нормальну роботу та скасовує роботу.
Xwtek

2

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

Ця концепція не дуже популярна, але чітко визначена і корисна. Якщо визначення здається вам занадто абстрактним, цей проект є вдалим прикладом того, як в Javascript визначено та реалізовано оперативне перетворення об’єктів JSON.



1

Ми використали повторно завантаження файлів і зберегли код серіалізації для "об'єктів" для зручної форми для збереження та відновлення всього стану об'єкта. Ми висуваємо ці серіалізовані об'єкти на стек скасування - разом з деякою інформацією про те, яка операція була виконана, і натякає на скасування цієї операції, якщо недостатньо інформації, зібраної з серіалізованих даних. Скасування та повторне повторення часто просто замінює один об'єкт іншим (теоретично).

Було багато помилок через покажчики (C ++) на об'єкти, які ніколи не були зафіксовані, як ви виконуєте кілька непарних послідовностей скасування повторень (ті місця, які не оновлювались, щоб безпечніше скасувати «ідентифікатори»). Помилки в цій області часто ... гммм ... цікаво.

Деякі операції можуть бути особливими випадками для швидкості / використання ресурсів - наприклад, розмір речей, переміщення речей.

Багаторазовий вибір також забезпечує цікаві ускладнення. На щастя, у нас уже була концепція групування в коді. Коментар Крістофера Джонсона щодо підпунктів досить близький до того, що ми робимо.


Це звучить все незрозуміліше, оскільки розмір вашої моделі збільшується.
Warren P

Яким чином? Цей підхід продовжує працювати без змін, оскільки до кожного об'єкта додаються нові "речі". Продуктивність може бути проблемою, оскільки серіалізована форма об'єктів збільшується в розмірах - але це не було основною проблемою. Система постійно розвивається протягом 20+ років і використовується у тисячах користувачів.
Aardvark

1

Мені довелося це робити, коли писав розв’язувач для гри-головоломки. Я робив кожен хід об'єктом Command, який містив достатньо інформації, щоб це можна було зробити або скасувати. У моєму випадку це було так само просто, як зберігання вихідної позиції та напрямку кожного руху. Потім я зберігав усі ці об’єкти в стеці, щоб програма могла легко скасувати стільки рухів, скільки потрібно під час зворотного відстеження.


1

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

Це дозволяє додавати функцію скасувати / повторити функцію у додаток, не застосовуючи шаблон. Він використовує шаблон запису для відстеження змін у вашій моделі, і він працює з шаблоном INotifyPropertyChanged, який також реалізований у PostSharp.

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


0

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


0

У першому розділі шаблонів дизайну (GoF, 1994) є приклад використання для реалізації відміни / повтору як шаблону дизайну.


0

Ви можете зробити свою початкову ідею виконавцем.

Використовуйте стійкі структури даних та дотримуйтесь переліку посилань на старий стан . (Але це справді працює, лише якщо операції з усіма даними вашого класу стану є незмінними, і всі операції над ним повертають нову версію --- але нову версію не потрібно копіювати, просто замініть копію змінених деталей -пишіть '.)


0

Тут я виявив, що шаблон команди дуже корисний. Замість того, щоб реалізувати кілька зворотних команд, я використовую відкат із затримкою виконання на другому примірнику мого API.

Такий підхід видається розумним, якщо ви хочете докласти малих зусиль із впровадження та легкої ремонтопридатності (і можете дозволити собі додаткову пам'ять для другої інстанції).

Тут див. Приклад: https://github.com/thilo20/Undo/


-1

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


Опублікуйте свої коментарі як відповідь, лише якщо ви впевнені в наданні рішень! Інакше віддайте перевагу опублікувати це як коментар під питанням! (якщо це не дозволяє зробити це зараз! будь ласка, зачекайте, поки ви отримаєте хорошу репутацію)
InfantPro'Aravind '11

-1

На мою думку, УНДО / РЕДО можна було б реалізувати двома способами широко. 1. Рівень команди (називається командним рівнем Скасувати / Повторити) 2. Рівень документа (називається глобальним Скасувати / Повторити)

Командний рівень: Оскільки багато відповідей вказують, це ефективно досягається за допомогою шаблону Memento. Якщо команда також підтримує перенесення дій, повтор легко підтримується.

Обмеження: Після того, як область дії команди вимкнена, скасувати / повторити неможливо, що призводить до скасування / повтору на рівні документа (глобального)

Я думаю, що ваш випадок впишеться в глобальний скасування / повтор, оскільки він підходить для моделі, яка передбачає багато місця в пам'яті. Також це підходить, щоб також вибірково скасовувати / повторювати. Існує два примітивні типи

  1. Скасувати / повторити всю пам'ять
  2. Рядок об’єкта Скасувати Повторити

У "Скасувати / Повторити всю пам'ять" вся пам'ять розглядається як підключені дані (наприклад, дерево, список або графік), а пам'яттю керує програма, а не ОС. Тож нові та видалені оператори, якщо в C ++ перевантажені, щоб містити більш конкретні структури для ефективної реалізації операцій, таких як. Якщо будь-який вузол модифікований, b. зберігання та очищення даних і т.д., спосіб його функціонування - це, в основному, копіювання всієї пам'яті (якщо припустити, що розподіл пам’яті вже оптимізовано та управляється програмою за допомогою вдосконалених алгоритмів) та зберігати їх у стеку. Якщо вимагається копія пам'яті, структура дерева копіюється, виходячи з необхідності мати дрібну або глибоку копію. Глибока копія робиться лише для тієї змінної, яка модифікована. Оскільки кожна змінна розміщена за допомогою спеціального розподілу, додаток має остаточне слово, коли його потрібно буде видалити, якщо виникне необхідність. Речі стають дуже цікавими, якщо нам доведеться розділити Скасувати / Повторити, коли так трапляється, що нам потрібно програмно-вибірково Скасувати / Повторити набір операцій. У цьому випадку лише цим новим змінним, або видаленим змінним або модифікованим змінним надається прапор, щоб скасувати / скасувати лише скасувати / повторно виправити ці пам'яті. Речі стають ще цікавішими, якщо нам потрібно зробити часткове скасування / повторення всередині об'єкта. У такому випадку використовується новіша ідея "Шаблон відвідувачів". Він називається "Скасувати / повторити рівень об'єкта" або видаленим змінним або модифікованим змінним надається прапор, щоб скасувати / скасувати лише скасувати / повторити цю пам'ять. Все стає ще цікавішим, якщо нам потрібно зробити часткове скасування / повторення всередині об'єкта. У такому випадку використовується новіша ідея "Шаблон відвідувачів". Він називається "Скасувати / повторити рівень об'єкта" або видаленим змінним або модифікованим змінним надається прапор, щоб скасувати / скасувати лише скасувати / повторити цю пам'ять. Все стає ще цікавішим, якщо нам потрібно зробити часткове скасування / повторення всередині об'єкта. У такому випадку використовується новіша ідея "Шаблон відвідувачів". Він називається "Скасувати / повторити рівень об'єкта"

  1. Рівень об'єкта Скасувати / Повторити: Коли викликається повідомлення про скасування / повтор, кожен об'єкт реалізує операцію потокової передачі, в якій стример отримує від об'єкта старі дані / нові дані, які запрограмовані. Дані, які не турбують, залишаються непорушеними. Кожен об'єкт отримує стример як аргумент, і всередині виклику UNDo / Redo він передає / передає потоки даних об'єкта.

І 1, і 2 можуть мати такі методи, як 1. BeforeUndo () 2. AfterUndo () 3. BeforeRedo () 4. AfterRedo (). Ці методи повинні бути опубліковані в базовій команді "Скасувати / повторити" (а не в контекстній команді), щоб усі об'єкти також реалізували ці методи для отримання конкретних дій.

Хорошою стратегією є створення гібриду 1 і 2. Приємним є те, що ці методи (1 і 2) самі використовують командні схеми

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.