Wzorzec projektowy dla mechanizmu cofania


117

Piszę narzędzie do modelowania strukturalnego na potrzeby inżynierii lądowej. Mam jedną ogromną klasę modelu reprezentującą cały budynek, która zawiera zbiory węzłów, elementów liniowych, obciążeń itp., Które są również klasami niestandardowymi.

Zakodowałem już mechanizm cofania, który zapisuje głęboką kopię po każdej modyfikacji modelu. Teraz zacząłem się zastanawiać, czy mógłbym kodować inaczej. Zamiast zapisywać głębokie kopie, mógłbym prawdopodobnie zapisać listę każdej akcji modyfikatora z odpowiednim modyfikatorem odwrotnym. Tak żebym mógł zastosować modyfikatory odwrotne do bieżącego modelu, aby cofnąć, lub modyfikatory, aby ponowić.

Mogę sobie wyobrazić, jak wykonywałbyś proste polecenia zmieniające właściwości obiektu itp. Ale co ze złożonymi poleceniami? Podobnie jak wstawianie nowych obiektów węzłów do modelu i dodawanie obiektów liniowych, które zachowują odniesienia do nowych węzłów.

Jak można by to wdrożyć?


Jeśli dodam komentarz „Undo Algorthim”, czy to sprawi, że będę mógł wyszukać „Undo Algorithm” i znaleźć to? Właśnie tego szukałem i znalazłem coś zamkniętego jako duplikat.
Peter Turner

siano, chcę również stworzyć cofnij / ponów w aplikacji, którą tworzymy. Używamy frameworka QT4 i potrzebujemy wielu złożonych operacji cofania / ponawiania. Zastanawiałem się, czy udało ci się użyć wzorca poleceń?
Ashika Umanga Umagiliya

2
@umanga: Udało się, ale nie było to łatwe. Najtrudniejsze było śledzenie referencji. Na przykład, gdy obiekt Frame zostanie usunięty, jego obiekty podrzędne: węzły, obciążenia działające na niego i wiele innych przypisań użytkowników musiało zostać zachowanych, aby zostały ponownie wstawione po cofnięciu. Ale niektóre z tych obiektów podrzędnych były współdzielone z innymi obiektami, a logika cofania / ponawiania stała się dość złożona. Gdyby model nie był tak duży, zachowałbym podejście memento; jest znacznie łatwiejszy do wdrożenia.
Ozgur Ozcitak

to fajny problem do pracy, zastanów się, jak robią to repozytoria kodu źródłowego, na przykład svn (zachowują różnice między zatwierdzeniami).
Alex

Odpowiedzi:


88

Większość przykładów, które widziałem, używa do tego wariantu wzorca polecenia . Każde działanie użytkownika, którego można cofnąć, otrzymuje własną instancję polecenia ze wszystkimi informacjami potrzebnymi do wykonania akcji i jej cofnięcia. Następnie możesz utrzymywać listę wszystkich wykonanych poleceń i wycofywać je jeden po drugim.


4
Tak właśnie działa mechanizm cofania zmian w kakao, NSUndoManager.
amrox

33

Myślę, że zarówno pamiątka, jak i polecenie nie są praktyczne, gdy masz do czynienia z modelem o rozmiarze i zakresie, jakie sugeruje PO. Działałyby, ale utrzymanie i rozbudowa wymagałoby dużo pracy.

W przypadku tego typu problemu myślę, że należy wbudować obsługę modelu danych, aby obsługiwać różnicowe punkty kontrolne dla każdego obiektu zaangażowanego w model. Zrobiłem to raz i zadziałało bardzo gładko. Największą rzeczą, którą musisz zrobić, jest unikanie bezpośredniego używania wskaźników lub odniesień w modelu.

Każde odwołanie do innego obiektu używa jakiegoś identyfikatora (np. Liczby całkowitej). Zawsze, gdy obiekt jest potrzebny, wyszukujesz bieżącą definicję obiektu w tabeli. Tabela zawiera połączoną listę dla każdego obiektu, która zawiera wszystkie poprzednie wersje, wraz z informacjami dotyczącymi punktu kontrolnego, w którym były one aktywne.

Implementacja cofnij / ponów jest prosta: wykonaj swoje działanie i ustal nowy punkt kontrolny; cofnij wszystkie wersje obiektów do poprzedniego punktu kontrolnego.

Wymaga to pewnej dyscypliny w kodzie, ale ma wiele zalet: nie potrzebujesz głębokich kopii, ponieważ robisz różnicowe przechowywanie stanu modelu; możesz określić ilość pamięci, której chcesz użyć (jest to bardzo ważne w przypadku modeli CAD) według liczby powtórzeń lub wykorzystanej pamięci; bardzo skalowalne i łatwe w utrzymaniu dla funkcji, które działają na modelu, ponieważ nie muszą nic robić, aby zaimplementować cofanie / ponawianie.


1
Jeśli używasz bazy danych (np. Sqlite) jako formatu pliku, może to nastąpić prawie automatycznie
Martin Beckett

4
Jeśli rozszerzysz to przez śledzenie zależności wprowadzonych przez zmiany w modelu, możesz potencjalnie mieć system cofania drzewa (tj. Jeśli zmienię szerokość dźwigara, a następnie popracuję nad oddzielnym komponentem, mogę wrócić i cofnąć dźwigar zmienia się bez utraty innych rzeczy). Interfejs użytkownika może być trochę nieporęczny, ale byłby znacznie potężniejszy niż tradycyjne cofanie liniowe.
Sumudu Fernando

Czy możesz dokładniej wyjaśnić ten pomysł id vs wskaźniki? Z pewnością adres wskaźnika / pamięci działa równie dobrze jak id?
paulm

@paulm: zasadniczo rzeczywiste dane są indeksowane przez (identyfikator, wersja). Wskaźniki odnoszą się do konkretnej wersji obiektu, ale chcesz odnieść się do aktualnego stanu obiektu, cokolwiek by to nie było, więc chcesz zająć się tym za pomocą identyfikatora, a nie (identyfikatora, wersji). Możesz go zmienić tak, aby przechowywać wskaźnik do tabeli (wersja => dane) i za każdym razem wybierać najnowszą, ale to zwykle szkodzi lokalności, gdy utrwalasz dane, trochę mętne obawy i utrudniają wykonać kilka typowych zapytań, więc nie jest to normalne rozwiązanie.
Chris Morgan,

17

Jeśli mówisz o GoF, wzorzec Memento w szczególności odnosi się do cofania.


7
Niezupełnie, to dotyczy jego początkowego podejścia. Prosi o alternatywne podejście. Początkowy przechowuje pełny stan dla każdego kroku, podczas gdy drugi przechowuje tylko "różnice".
Andrei Rînea

15

Jak powiedzieli inni, wzorzec poleceń jest bardzo skuteczną metodą implementacji Cofnij / Ponów. Jest jednak ważna zaleta, o której chciałbym wspomnieć we wzorcu poleceń.

Podczas implementowania cofania / ponawiania przy użyciu wzorca poleceń można uniknąć dużej ilości powielanego kodu poprzez abstrakcję (do pewnego stopnia) operacji wykonywanych na danych i wykorzystanie tych operacji w systemie cofania / ponawiania. Na przykład w edytorze tekstu wytnij i wklej są komendami uzupełniającymi (poza zarządzaniem schowkiem). Innymi słowy, operacją cofnięcia wycięcia jest wklejanie, a operacją cofnięcia operacji wklejania jest wycinanie. Dotyczy to znacznie prostszych operacji, takich jak wpisywanie i usuwanie tekstu.

Najważniejsze jest to, że możesz użyć swojego systemu cofania / ponawiania jako podstawowego systemu poleceń dla swojego edytora. Zamiast pisać system, np. „Utwórz obiekt cofania, modyfikuj dokument”, można „utworzyć obiekt cofania, wykonać operację ponowienia na cofnięciu obiektu, aby zmodyfikować dokument”.

Teraz, trzeba przyznać, wiele osób myśli sobie: „No cóż, czy nie jest to sednem wzorca dowodzenia?” Tak, ale widziałem zbyt wiele systemów poleceń, które mają dwa zestawy poleceń, jeden do natychmiastowych operacji, a drugi do cofania / ponawiania. Nie mówię, że nie będzie poleceń, które są specyficzne dla operacji natychmiastowych i cofnij / ponów, ale ograniczenie duplikacji sprawi, że kod będzie łatwiejszy w utrzymaniu.


1
Nigdy nie myślałem o tym, pasteże cut^ -1.
Lenar Hoyt

8

Możesz chcieć odwołać się do kodu Paint.NET w celu ich cofnięcia - mają naprawdę fajny system cofania. Prawdopodobnie jest to nieco prostsze niż to, czego będziesz potrzebować, ale może dać ci kilka pomysłów i wskazówek.

-Adam


4
Właściwie kod Paint.NET nie jest już dostępny, ale możesz uzyskać rozwidlony code.google.com/p/paint-mono
Igor Brejc

7

Może to być przypadek, w którym ma zastosowanie CSLA . Został zaprojektowany, aby zapewnić złożoną obsługę cofania obiektów w aplikacjach Windows Forms.


6

Z powodzeniem zaimplementowałem złożone systemy cofania za pomocą wzorca Memento - bardzo łatwe i ma tę zaletę, że w naturalny sposób zapewnia również framework Redo. Bardziej subtelną korzyścią jest to, że zagregowane akcje mogą być również zawarte w jednym cofnięciu.

Krótko mówiąc, masz dwa stosy pamiątek. Jeden dla Cofnij, drugi dla Ponów. Każda operacja tworzy nową memento, która idealnie będzie stanowić wezwanie do zmiany stanu twojego modelu, dokumentu (lub cokolwiek innego). To zostanie dodane do stosu cofania. Kiedy wykonujesz operację cofania, oprócz wykonania akcji Cofnij na obiekcie Memento, aby ponownie zmienić model, zdejmujesz obiekt ze stosu Cofnij i wsuwasz go bezpośrednio na stos Ponów.

Sposób implementacji metody zmiany stanu dokumentu zależy całkowicie od implementacji. Jeśli możesz po prostu wykonać wywołanie API (np. ChangeColour (r, g, b)), poprzedź je zapytaniem, aby pobrać i zapisać odpowiedni stan. Ale wzorzec będzie również obsługiwał tworzenie głębokich kopii, migawek pamięci, tworzenie plików tymczasowych itp. - wszystko zależy od Ciebie, ponieważ jest to po prostu implementacja metody wirtualnej.

Aby wykonać zagregowane akcje (np. Użytkownik Shift-wybiera ładunek obiektów do wykonania operacji, takich jak usuwanie, zmiana nazwy, zmiana atrybutu), twój kod tworzy nowy stos cofania jako pojedynczą pamiątkę i przekazuje go do faktycznej operacji do dodać poszczególne operacje do. Więc twoje metody akcji nie muszą (a) mieć globalnego stosu, o który trzeba się martwić i (b) mogą być zakodowane tak samo, niezależnie od tego, czy są wykonywane oddzielnie, czy jako część jednej operacji agregującej.

Wiele systemów cofania znajduje się tylko w pamięci, ale myślę, że możesz utrwalić stos cofania, jeśli chcesz.


5

Właśnie czytałem o schemacie poleceń w mojej książce o programowaniu zwinnym - może to ma potencjał?

Każde polecenie może implementować interfejs poleceń (który ma metodę Execute ()). Jeśli chcesz cofnąć, możesz dodać metodę Cofnij.

więcej informacji tutaj


4

Jestem z Mendeltem Siebengą w kwestii faktu, że powinieneś używać wzorca dowodzenia. Wzorem, którego użyłeś, był wzór Memento, który z czasem może stać się bardzo marnotrawny.

Ponieważ pracujesz nad aplikacją intensywnie wykorzystującą pamięć, powinieneś być w stanie określić, ile pamięci może zajmować silnik cofania, ile poziomów cofania jest zapisywanych lub jaką pamięć masową, w której będą one utrwalane. Jeśli tego nie zrobisz, wkrótce napotkasz błędy wynikające z braku pamięci urządzenia.

Radziłbym sprawdzić, czy istnieje framework, który utworzył już model cofania w wybranym języku programowania / frameworku. Fajnie jest wymyślać nowe rzeczy, ale lepiej jest wziąć coś już napisanego, debugowanego i przetestowanego w prawdziwych scenariuszach. Byłoby pomocne, gdybyś dodał to, w czym to piszesz, aby ludzie mogli polecić znane im frameworki.


3

Projekt Codeplex :

Jest to prosta struktura umożliwiająca dodanie funkcji Cofnij / Ponów do aplikacji, oparta na klasycznym wzorcu projektowania poleceń. Obsługuje akcje scalające, transakcje zagnieżdżone, opóźnione wykonanie (wykonanie na najwyższym poziomie zatwierdzenia transakcji) i możliwą nieliniową historię cofania (gdzie można wybrać wiele akcji do ponownego wykonania).


2

Większość przykładów, które przeczytałem, robi to za pomocą polecenia lub wzorca memento. Ale możesz to zrobić również bez wzorców projektowych dzięki prostej strukturze deque .


Co byś umieścił w deque?

W moim przypadku podałem aktualny stan operacji, dla których chciałem cofnąć / ponawiać działanie. Mając dwa deques (cofnij / ponów), robię cofnięcie w kolejce cofania (zdejmuję pierwszy element) i wstawiam go do ponownego usunięcia z kolejki. Jeśli liczba elementów w usunięciu z kolejki przekracza preferowany rozmiar, zdejmuję element z ogona.
Patrik Svensson,

2
To, co opisujesz, JEST wzorcem projektowym :). Problem z tym podejściem polega na tym, że twój stan zajmuje dużo pamięci - utrzymanie kilkudziesięciu wersji stanu staje się wtedy niepraktyczne lub nawet niemożliwe.
Igor Brejc

Lub możesz zapisać parę zamknięć reprezentujących normalne i cofnięte operacje.
Xwtek

2

Sprytnym sposobem obsługi cofania, który sprawiłby, że oprogramowanie nadaje się również do współpracy wielu użytkowników, polega na wdrożeniu transformacji operacyjnej struktury danych.

Ta koncepcja nie jest zbyt popularna, ale dobrze zdefiniowana i użyteczna. Jeśli definicja wydaje Ci się zbyt abstrakcyjna, ten projekt jest udanym przykładem tego, jak transformacja operacyjna dla obiektów JSON jest definiowana i implementowana w JavaScript



1

Ponownie wykorzystaliśmy plik ładowania i zapisywania kodu serializacji dla „obiektów”, aby uzyskać wygodny formularz do zapisywania i odtwarzania całego stanu obiektu. Umieszczamy te zserializowane obiekty na stosie cofania - wraz z pewnymi informacjami o tym, jaka operacja została wykonana, i wskazówkami dotyczącymi cofania tej operacji, jeśli nie ma wystarczającej ilości informacji zebranych z serializowanych danych. Cofnij i Ponów często polega na zamianie jednego obiektu na inny (w teorii).

Było wiele błędów związanych ze wskaźnikami (C ++) do obiektów, które nigdy nie zostały naprawione podczas wykonywania pewnych dziwnych sekwencji cofania powtórzeń (te miejsca nie zostały zaktualizowane, aby bezpieczniej cofnąć świadome „identyfikatory”). Błędy w tej okolicy często ... hmm ... interesujące.

Niektóre operacje mogą być specjalnymi przypadkami związanymi z prędkością / zużyciem zasobów - na przykład wymiarowanie rzeczy, przenoszenie rzeczy.

Wielokrotny wybór również powoduje pewne interesujące komplikacje. Na szczęście w kodzie mieliśmy już koncepcję grupowania. Komentarz Kristophera Johnsona na temat podpozycji jest bardzo zbliżony do tego, co robimy.


Wydaje się to coraz bardziej niewykonalne, gdy rozmiar twojego modelu rośnie.
Warren P

W jaki sposób? To podejście działa bez zmian, ponieważ do każdego obiektu dodawane są nowe „rzeczy”. Wydajność może być problemem, ponieważ serializowana forma obiektów rośnie, ale nie stanowi to poważnego problemu. System jest rozwijany od ponad 20 lat i jest używany przez tysiące użytkowników.
Aardvark

1

Musiałem to zrobić, pisząc rozwiązanie do gry logicznej typu peg-jump. Każdemu ruchowi nadałem obiekt Command, który zawierał wystarczającą ilość informacji, aby można było go wykonać lub cofnąć. W moim przypadku było to tak proste, jak zapisanie pozycji wyjściowej i kierunku każdego ruchu. Następnie umieściłem wszystkie te obiekty w stosie, aby program mógł łatwo cofnąć tyle ruchów, ile potrzebował podczas cofania.


1

Możesz spróbować gotowej implementacji wzorca Undo / Redo w PostSharp. https://www.postsharp.net/model/undo-redo

Umożliwia dodanie funkcji cofania / ponawiania do aplikacji bez samodzielnego wdrażania wzorca. Używa wzorca Recordable do śledzenia zmian w modelu i działa z wzorcem INotifyPropertyChanged, który jest również zaimplementowany w PostSharp.

Otrzymujesz kontrolki interfejsu użytkownika i możesz zdecydować, jaka będzie nazwa i szczegółowość każdej operacji.


0

Kiedyś pracowałem nad aplikacją, w której wszystkie zmiany wprowadzone poleceniem do modelu aplikacji (np. CDocument ... używaliśmy MFC) były utrwalane na końcu polecenia poprzez aktualizację pól w wewnętrznej bazie danych utrzymywanej w modelu. Nie musieliśmy więc pisać oddzielnego kodu cofania / ponawiania dla każdej akcji. Stos cofania po prostu zapamiętywał klucze główne, nazwy pól i stare wartości za każdym razem, gdy rekord był zmieniany (na końcu każdego polecenia).


0

Pierwsza sekcja wzorców projektowych (GoF, 1994) zawiera przypadek użycia do implementacji cofania / ponawiania jako wzorca projektowego.


0

Możesz sprawić, że Twój początkowy pomysł będzie skuteczny.

Używaj trwałych struktur danych i trzymaj się listy odniesień do starego stanu . (Ale to naprawdę działa tylko wtedy, gdy operacje wszystkie dane w Twojej klasie stanu są niezmienne, a wszystkie operacje na nich zwracają nową wersję - ale nowa wersja nie musi być głęboką kopią, po prostu zastąp kopię zmienionych części -on-write '.)


0

Zauważyłem, że wzór Command jest tutaj bardzo przydatny. Zamiast implementować kilka poleceń odwrotnych, używam wycofywania z opóźnionym wykonaniem na drugim wystąpieniu mojego interfejsu API.

Takie podejście wydaje się rozsądne, jeśli zależy Ci na niewielkim wysiłku implementacji i łatwej obsłudze (i możesz pozwolić sobie na dodatkową pamięć dla drugiej instancji).

Zobacz tutaj przykład: https://github.com/thilo20/Undo/


-1

Nie wiem, czy to ci się przyda, ale kiedy musiałem zrobić coś podobnego w jednym z moich projektów, skończyło się na pobraniu UndoEngine z http://www.undomadeeasy.com - cudownego silnika i naprawdę nie przejmowałem się zbytnio tym, co było pod maską - po prostu działało.


Prosimy o umieszczanie komentarzy jako odpowiedź tylko wtedy, gdy jesteś pewien, że możesz podać rozwiązania! W przeciwnym razie wolę zamieścić to jako komentarz pod pytaniem! (jeśli nie pozwala na to teraz! poczekaj, aż uzyskasz dobrą reputację)
InfantPro'Aravind '

-1

Moim zdaniem UNDO / REDO można by wdrożyć szeroko na 2 sposoby. 1. Poziom polecenia (nazywany poziomem polecenia Cofnij / Ponów) 2. Poziom dokumentu (nazywany globalnym Cofnij / Ponów)

Poziom poleceń: jak wskazuje wiele odpowiedzi, można to skutecznie osiągnąć za pomocą wzorca Memento. Jeśli polecenie obsługuje również kronikowanie akcji, łatwo jest wykonać ponowienie.

Ograniczenie: Po wygaśnięciu zakresu polecenia cofnięcie / ponowienie jest niemożliwe, co prowadzi do poziomu dokumentu (globalnego) cofnij / ponów

Myślę, że Twój przypadek pasowałby do globalnego cofania / ponawiania, ponieważ jest odpowiedni dla modelu, który wymaga dużo miejsca w pamięci. Jest to również odpowiednie do selektywnego cofania / ponawiania. Istnieją dwa typy pierwotne

  1. Cofnij / ponów całą pamięć
  2. Poziom obiektu Cofnij Ponów

W „Cofnij / Ponów całą pamięć” cała pamięć jest traktowana jako połączone dane (takie jak drzewo, lista lub wykres), a pamięcią zarządza aplikacja, a nie system operacyjny. Tak więc operatory new i delete, jeśli w C ++ są przeciążone, aby zawierały bardziej szczegółowe struktury, aby skutecznie implementować operacje, takie jak. Jeśli jakikolwiek węzeł zostanie zmodyfikowany, b. przechowywanie i kasowanie danych itp., Funkcjonuje w zasadzie kopiowanie całej pamięci (zakładając, że alokacja pamięci jest już zoptymalizowana i zarządzana przez aplikację przy użyciu zaawansowanych algorytmów) i przechowywanie jej w stosie. Jeśli zażądano kopii pamięci, struktura drzewa jest kopiowana w oparciu o potrzebę posiadania płytkiej lub głębokiej kopii. Głęboka kopia jest tworzona tylko dla tej zmiennej, która jest modyfikowana. Ponieważ każda zmienna jest przydzielana za pomocą przydziału niestandardowego, aplikacja ma ostatnie słowo, kiedy należy ją usunąć w razie potrzeby. Sprawy stają się bardzo interesujące, jeśli musimy podzielić Cofnij / Ponów na partycje, gdy tak się stanie, że musimy programowo-selektywnie Cofnij / Ponów zestaw operacji. W tym przypadku tylko te nowe zmienne lub usunięte zmienne lub zmodyfikowane zmienne otrzymują flagę, dzięki czemu Cofnij / Ponów tylko cofa / przywraca pamięć Rzeczy stają się jeszcze bardziej interesujące, jeśli musimy wykonać częściowe Cofnij / Ponów wewnątrz obiektu. W takim przypadku używana jest nowsza koncepcja „wzorca gości”. Nazywa się to „Cofnij / ponów na poziomie obiektu” lub usunięte zmienne lub zmodyfikowane zmienne otrzymują flagę, dzięki czemu Cofnij / Ponów tylko cofa / przywraca tę pamięć Sprawy stają się jeszcze bardziej interesujące, jeśli musimy wykonać częściowe Cofnij / Ponów wewnątrz obiektu. W takim przypadku używana jest nowsza koncepcja „wzorca gości”. Nazywa się to „Cofnij / ponów na poziomie obiektu” lub usunięte zmienne lub zmodyfikowane zmienne otrzymują flagę, dzięki czemu Cofnij / Ponów tylko cofa / przywraca pamięć Rzeczy stają się jeszcze bardziej interesujące, jeśli musimy wykonać częściowe Cofnij / Ponów wewnątrz obiektu. W takim przypadku używana jest nowsza koncepcja „wzorca gości”. Nazywa się to „Cofnij / ponów na poziomie obiektu”

  1. Cofnij / ponów na poziomie obiektu: gdy wywoływane jest powiadomienie o cofnięciu / ponownym wykonaniu, każdy obiekt implementuje operację przesyłania strumieniowego, w której urządzenie do przekazu strumieniowego pobiera z obiektu stare / nowe dane, które są zaprogramowane. Dane, które nie zostaną naruszone, pozostają nienaruszone. Każdy obiekt otrzymuje streamer jako argument i wewnątrz wywołania UNDo / Redo przesyła strumieniowo / usuwa strumieniowo dane obiektu.

Zarówno 1, jak i 2 mogą mieć takie metody, jak 1. BeforeUndo () 2. AfterUndo () 3. BeforeRedo () 4. AfterRedo (). Metody te muszą być opublikowane w podstawowym poleceniu Cofnij / Ponów (nie w poleceniu kontekstowym), aby wszystkie obiekty również implementowały te metody w celu uzyskania określonej akcji.

Dobrą strategią jest stworzenie hybrydy 1 i 2. Piękno polega na tym, że te metody (1 i 2) same używają wzorców poleceń

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.