Frage

Ich schreibe ein Strukturmodellierungstool für eine Anwendung im Tiefbau.Ich habe eine riesige Modellklasse, die das gesamte Gebäude darstellt und Sammlungen von Knoten, Linienelementen, Lasten usw. umfasst.Dies sind ebenfalls benutzerdefinierte Klassen.

Ich habe bereits eine Undo-Engine programmiert, die nach jeder Änderung am Modell eine tiefe Kopie speichert.Jetzt begann ich darüber nachzudenken, ob ich anders hätte programmieren können.Anstatt die Tiefenkopien zu speichern, könnte ich vielleicht eine Liste jeder Modifikatoraktion mit einem entsprechenden Umkehrmodifikator speichern.Damit ich die umgekehrten Modifikatoren auf das aktuelle Modell anwenden kann, um sie rückgängig zu machen, oder die Modifikatoren, um sie wiederherzustellen.

Ich kann mir vorstellen, wie Sie einfache Befehle ausführen würden, die Objekteigenschaften usw. ändern.Aber wie wäre es mit komplexen Befehlen?Zum Beispiel das Einfügen neuer Knotenobjekte in das Modell und das Hinzufügen einiger Linienobjekte, die Verweise auf die neuen Knoten behalten.

Wie würde man das umsetzen?

War es hilfreich?

Lösung

Die meisten Beispiele, die ich gesehen habe, verwenden eine Variante davon Befehlsmuster dafür.Jede Benutzeraktion, die rückgängig gemacht werden kann, erhält eine eigene Befehlsinstanz mit allen Informationen, um die Aktion auszuführen und rückgängig zu machen.Anschließend können Sie eine Liste aller ausgeführten Befehle verwalten und diese nacheinander rückgängig machen.

Andere Tipps

Ich denke, dass sowohl Erinnerung als auch Befehl nicht praktikabel sind, wenn es sich um ein Modell der Größe und des Umfangs handelt, die das OP impliziert.Sie würden funktionieren, aber es wäre eine Menge Arbeit, sie zu warten und zu erweitern.

Für diese Art von Problem müssen Sie meines Erachtens eine Unterstützung in Ihr Datenmodell einbauen, um differenzielle Prüfpunkte zu unterstützen jedes Objekt am Modell beteiligt.Ich habe das einmal gemacht und es hat sehr gut funktioniert.Das Wichtigste, was Sie tun müssen, ist, die direkte Verwendung von Zeigern oder Referenzen im Modell zu vermeiden.

Jeder Verweis auf ein anderes Objekt verwendet einen Bezeichner (z. B. eine Ganzzahl).Wann immer das Objekt benötigt wird, suchen Sie in einer Tabelle nach der aktuellen Definition des Objekts.Die Tabelle enthält eine verknüpfte Liste für jedes Objekt, die alle vorherigen Versionen enthält, zusammen mit Informationen darüber, für welchen Prüfpunkt sie aktiv waren.

Die Implementierung von Rückgängig/Wiederherstellen ist einfach:Führen Sie Ihre Aktion aus und richten Sie einen neuen Kontrollpunkt ein.Rollback aller Objektversionen auf den vorherigen Prüfpunkt.

Es erfordert etwas Disziplin im Code, hat aber viele Vorteile:Sie benötigen keine tiefen Kopien, da Sie den Modellstatus differenziell speichern.Sie können die Menge an Speicher, die Sie verwenden möchten, festlegen (sehr wichtig für Dinge wie CAD-Modelle) entweder nach Anzahl der Wiederholungen oder nach verwendetem Speicher;Sehr skalierbar und wartungsarm für die Funktionen, die auf dem Modell ausgeführt werden, da sie nichts tun müssen, um Rückgängig/Wiederherstellen zu implementieren.

Wenn Sie von GoF sprechen, dann Erinnerung Das Muster befasst sich speziell mit dem Rückgängigmachen.

Wie andere bereits erwähnt haben, ist das Befehlsmuster eine sehr leistungsstarke Methode zum Implementieren von Rückgängig/Wiederherstellen.Es gibt jedoch einen wichtigen Vorteil des Befehlsmusters, den ich erwähnen möchte.

Wenn Sie das Rückgängigmachen/Wiederherstellen mithilfe des Befehlsmusters implementieren, können Sie große Mengen duplizierten Codes vermeiden, indem Sie die an den Daten ausgeführten Vorgänge (bis zu einem gewissen Grad) abstrahieren und diese Vorgänge im Rückgängig-/Wiederholen-System verwenden.Beispielsweise sind Ausschneiden und Einfügen in einem Texteditor ergänzende Befehle (abgesehen von der Verwaltung der Zwischenablage).Mit anderen Worten, der Rückgängig-Vorgang für einen Ausschnitt ist „Einfügen“ und der Rückgängig-Vorgang für ein Einfügen ist „Ausschneiden“.Dies gilt für viel einfachere Vorgänge wie das Eingeben und Löschen von Text.

Der Schlüssel hier ist, dass Sie Ihr Rückgängig-/Wiederholen-System als primäres Befehlssystem für Ihren Editor verwenden können.Anstatt das System wie „Rückgängig-Objekt erstellen, Dokument ändern“ zu schreiben, können Sie „Rückgängig-Objekt erstellen, Wiederherstellungsvorgang für Rückgängig-Objekt ausführen, um das Dokument zu ändern“ verwenden.

Zugegebenermaßen denken viele Menschen für sich selbst: "Nun, duh, ist nicht Teil des Befehlsmusters?" Ja, aber ich habe zu viele Befehlssysteme gesehen, die zwei Befehlesätze haben, einen für sofortige Operationen und einen anderen Satz für Rückgänger/Redo.Ich sage nicht, dass es keine Befehle geben wird, die sich speziell auf sofortige Vorgänge und das Rückgängigmachen/Wiederherstellen beziehen, aber durch die Reduzierung der Duplizierung wird der Code besser wartbar.

Vielleicht möchten Sie sich auf die beziehen Paint.NET-Code für ihr Rückgängigmachen - sie haben ein wirklich schönes Rückgängig-System.Es ist wahrscheinlich etwas einfacher als das, was Sie brauchen, aber es könnte Ihnen einige Ideen und Richtlinien geben.

-Adam

Dies könnte ein Fall sein, in dem CSLA anwendbar.Es wurde entwickelt, um komplexe Rückgängig-Unterstützung für Objekte in Windows Forms-Anwendungen bereitzustellen.

Ich habe komplexe Undo-Systeme erfolgreich mit dem Memento-Muster implementiert – sehr einfach und mit dem Vorteil, dass natürlich auch ein Redo-Framework bereitgestellt wird.Ein subtilerer Vorteil besteht darin, dass aggregierte Aktionen auch in einem einzigen Rückgängig-Vorgang enthalten sein können.

Kurz gesagt, Sie haben zwei Stapel Erinnerungsobjekte.Eines zum Rückgängigmachen, das andere zum Wiederherstellen.Bei jedem Vorgang wird ein neues Erinnerungsstück erstellt, bei dem es sich im Idealfall um einige Aufrufe zur Änderung des Status Ihres Modells, Dokuments (oder was auch immer) handelt.Dies wird zum Rückgängig-Stapel hinzugefügt.Wenn Sie einen Rückgängig-Vorgang ausführen, führen Sie nicht nur die Rückgängig-Aktion für das Memento-Objekt aus, um das Modell wieder zu ändern, sondern entfernen auch das Objekt vom Rückgängig-Stapel und verschieben es direkt auf den Wiederherstellungs-Stapel.

Wie die Methode zum Ändern des Status Ihres Dokuments implementiert wird, hängt vollständig von Ihrer Implementierung ab.Wenn Sie einfach einen API-Aufruf durchführen können (z. B.ChangeColor(r,g,b)) und stellen Sie ihm dann eine Abfrage voran, um den entsprechenden Status abzurufen und zu speichern.Das Muster unterstützt aber auch das Erstellen tiefer Kopien, Speicher-Snapshots, die Erstellung temporärer Dateien usw. – es liegt ganz bei Ihnen, da es sich lediglich um eine virtuelle Methodenimplementierung handelt.

Um aggregierte Aktionen durchzuführen (z. B.Wenn der Benutzer bei gedrückter Umschalttaste eine Menge Objekte auswählt, an denen eine Operation ausgeführt werden soll, z. B. Löschen, Umbenennen, Attribut ändern), erstellt Ihr Code einen neuen Rückgängig-Stapel als einzelne Erinnerung und übergibt diesen an die eigentliche Operation, um die einzelnen Operationen hinzuzufügen.Ihre Aktionsmethoden müssen sich also (a) nicht um einen globalen Stack kümmern und (b) können gleich codiert werden, unabhängig davon, ob sie isoliert oder als Teil einer Aggregatoperation ausgeführt werden.

Viele Rückgängig-Systeme sind nur im Speicher verfügbar, aber Sie könnten den Rückgängig-Stack beibehalten, wenn Sie möchten, denke ich.

Ich habe gerade in meinem Buch zur agilen Entwicklung etwas über das Befehlsmuster gelesen – vielleicht hat das Potenzial?

Sie können festlegen, dass jeder Befehl die Befehlsschnittstelle implementiert (die über eine Execute()-Methode verfügt).Wenn Sie den Vorgang rückgängig machen möchten, können Sie eine Rückgängig-Methode hinzufügen.

Mehr Info Hier

ich bin mit Mendelt Siebenga darauf, dass Sie das Befehlsmuster verwenden sollten.Das Muster, das Sie verwendet haben, war das Memento-Muster, das mit der Zeit sehr verschwenderisch werden kann und wird.

Da Sie an einer speicherintensiven Anwendung arbeiten, sollten Sie in der Lage sein, entweder anzugeben, wie viel Speicher die Rückgängig-Engine beanspruchen darf, wie viele Rückgängig-Ebenen gespeichert werden oder welchen Speicher sie beibehalten sollen.Wenn Sie dies nicht tun, werden Sie bald mit Fehlern konfrontiert, die darauf zurückzuführen sind, dass der Computer nicht über genügend Speicher verfügt.

Ich würde Ihnen raten, zu prüfen, ob es ein Framework gibt, das bereits ein Modell für Rückgängigmachungen in der Programmiersprache/dem Framework Ihrer Wahl erstellt hat.Es ist schön, neue Dinge zu erfinden, aber es ist besser, etwas zu nehmen, das bereits geschrieben, debuggt und in realen Szenarien getestet wurde.Es wäre hilfreich, wenn Sie hinzufügen würden, in was Sie dies schreiben, damit die Leute Frameworks empfehlen können, die sie kennen.

Codeplex-Projekt:

Es handelt sich um ein einfaches Framework zum Hinzufügen von Undo/Redo-Funktionen zu Ihren Anwendungen, basierend auf dem klassischen Command-Entwurfsmuster.Es unterstützt Zusammenführungsaktionen, verschachtelte Transaktionen, verzögerte Ausführung (Ausführung beim Festschreiben einer Transaktion auf oberster Ebene) und einen möglichen nichtlinearen Rückgängig-Verlauf (wobei Sie zwischen mehreren Aktionen zum Wiederherstellen wählen können).

Bei den meisten Beispielen, die ich gelesen habe, wird entweder das Befehls- oder das Memento-Muster verwendet.Sie können es aber auch ohne Designmuster mit einem einfachen machen Deque-Struktur.

Eine clevere Möglichkeit, das Rückgängigmachen zu handhaben und Ihre Software auch für die Zusammenarbeit mehrerer Benutzer geeignet zu machen, ist die Implementierung einer betriebliche Transformation der Datenstruktur.

Dieses Konzept ist nicht sehr beliebt, aber gut definiert und nützlich.Wenn Ihnen die Definition zu abstrakt erscheint, dieses Projekt ist ein gelungenes Beispiel dafür, wie eine operative Transformation für JSON-Objekte in Javascript definiert und implementiert wird

Als Referenz finden Sie hier eine einfache Implementierung des Befehlsmusters für Rückgängig/Wiederherstellen in C#: Einfaches Rückgängig-/Wiederholen-System für C#.

Wir haben den Dateilade- und Speicherserialisierungscode für „Objekte“ wiederverwendet, um ein praktisches Formular zum Speichern und Wiederherstellen des gesamten Status eines Objekts zu erhalten.Wir verschieben diese serialisierten Objekte auf den Rückgängig-Stapel – zusammen mit einigen Informationen darüber, welcher Vorgang ausgeführt wurde, und Hinweisen zum Rückgängigmachen dieses Vorgangs, wenn aus den serialisierten Daten nicht genügend Informationen gewonnen werden können.Beim Rückgängigmachen und Wiederherstellen wird (theoretisch) oft nur ein Objekt durch ein anderes ersetzt.

Es gab viele VIELE Fehler aufgrund von Zeigern (C++) auf Objekte, die nie repariert wurden, da Sie einige seltsame Undo-Redo-Sequenzen ausführen (die Stellen, die nicht auf sicherere Undo-fähige „Identifikatoren“ aktualisiert wurden).In diesem Bereich gibt es häufig Fehler ... ähm ...interessant.

Bei einigen Vorgängen kann es sich um Sonderfälle im Hinblick auf die Geschwindigkeit/Ressourcennutzung handeln, z. B. das Anpassen der Größe von Dingen oder das Verschieben von Dingen.

Die Mehrfachauswahl bietet auch einige interessante Komplikationen.Glücklicherweise hatten wir im Code bereits ein Gruppierungskonzept.Der Kommentar von Kristopher Johnson zu Unterpunkten kommt dem, was wir tun, ziemlich nahe.

Ich musste dies tun, als ich einen Löser für ein Peg-Jump-Puzzlespiel schrieb.Ich habe jede Bewegung zu einem Befehlsobjekt gemacht, das genügend Informationen enthielt, um sie entweder ausführen oder rückgängig machen zu können.In meinem Fall war das so einfach wie das Speichern der Startposition und der Richtung jeder Bewegung.Anschließend habe ich alle diese Objekte in einem Stapel gespeichert, sodass das Programm beim Zurückverfolgen problemlos so viele Bewegungen rückgängig machen konnte, wie es benötigte.

Sie können die vorgefertigte Implementierung des Undo/Redo-Musters in PostSharp ausprobieren. https://www.postsharp.net/model/undo-redo

Damit können Sie Ihrer Anwendung Rückgängig-/Wiederherstellen-Funktionen hinzufügen, ohne das Muster selbst implementieren zu müssen.Es verwendet das Recordable-Muster, um die Änderungen in Ihrem Modell zu verfolgen, und es funktioniert mit dem INotifyPropertyChanged-Muster, das auch in PostSharp implementiert ist.

Sie erhalten UI-Steuerelemente und können den Namen und die Granularität jedes Vorgangs festlegen.

Ich habe einmal an einer Anwendung gearbeitet, in der alle Änderungen, die durch einen Befehl am Modell der Anwendung vorgenommen wurden (d. h.CDokument...(wir verwendeten MFC) wurden am Ende des Befehls beibehalten, indem Felder in einer internen Datenbank aktualisiert wurden, die im Modell verwaltet wird.Wir mussten also nicht für jede Aktion einen separaten Rückgängig-/Wiederholen-Code schreiben.Der Rückgängig-Stack merkte sich einfach jedes Mal die Primärschlüssel, Feldnamen und alten Werte, wenn ein Datensatz geändert wurde (am Ende jedes Befehls).

Der erste Abschnitt von Design Patterns (GoF, 1994) enthält einen Anwendungsfall für die Implementierung des Rückgängigmachens/Wiederholens als Designmuster.

Sie können Ihre ursprüngliche Idee in die Tat umsetzen.

Verwenden persistente Datenstrukturen, und bleiben Sie dabei, a Liste der Verweise auf den alten Zustand in der Umgebung.(Aber das funktioniert nur dann wirklich, wenn Operationen an allen Daten in Ihrer Zustandsklasse unveränderlich sind und alle Operationen darauf eine neue Version zurückgeben – aber die neue Version muss keine tiefe Kopie sein, sondern ersetzt einfach die geänderten Teile durch die Kopie -on-write'.)

Ich habe festgestellt, dass das Befehlsmuster hier sehr nützlich ist.Anstatt mehrere umgekehrte Befehle zu implementieren, verwende ich Rollback mit verzögerter Ausführung auf einer zweiten Instanz meiner API.

Dieser Ansatz erscheint sinnvoll, wenn Sie einen geringen Implementierungsaufwand und eine einfache Wartbarkeit wünschen (und sich den zusätzlichen Speicher für die 2. Instanz leisten können).

Ein Beispiel finden Sie hier:https://github.com/thilo20/Undo/

Ich weiß nicht, ob das für Sie von Nutzen sein wird, aber als ich bei einem meiner Projekte etwas Ähnliches tun musste, habe ich mir letztendlich UndoEngine von heruntergeladen http://www.undomadeeasy.com - ein wunderbarer Motor und mir war eigentlich egal, was sich unter der Motorhaube befand - es funktionierte einfach.

Meiner Meinung nach könnte UNDO/REDO im Großen und Ganzen auf zwei Arten implementiert werden.1.Befehlsstufe (als Befehlsebene und OSO/Redo bezeichnet) 2.Dokumentebene (genannt globales Rückgängigmachen/Wiederherstellen)

Befehlsebene:Wie aus vielen Antworten hervorgeht, wird dies mithilfe des Memento-Musters effizient erreicht.Wenn der Befehl auch das Journalisieren der Aktion unterstützt, ist ein Wiederherstellen problemlos möglich.

Einschränkung:Sobald der Gültigkeitsbereich des Befehls abgelaufen ist, ist das Rückgängigmachen/Wiederherstellen nicht mehr möglich, was zu einem (globalen) Rückgängigmachen/Wiederherstellen auf Dokumentebene führt

Ich schätze, Ihr Fall würde in die globale Funktion „Rückgängig/Wiederherstellen“ passen, da er für ein Modell geeignet ist, das viel Speicherplatz benötigt.Dies eignet sich auch zum selektiven Rückgängigmachen/Wiederherstellen.Es gibt zwei primitive Typen

  1. Den gesamten Speicher rückgängig machen/wiederholen
  2. Objektebene Rückgängigmachen Wiederholen

Bei „Rückgängig/Wiederherstellen des gesamten Speichers“ wird der gesamte Speicher als verbundene Daten behandelt (z. B. ein Baum, eine Liste oder ein Diagramm) und der Speicher wird von der Anwendung und nicht vom Betriebssystem verwaltet.Daher sind neue und Löschoperatoren in C++ überladen, um spezifischere Strukturen zu enthalten, um Operationen wie a effektiv zu implementieren.Wenn ein Knoten geändert wird, b.Das Halten und Löschen von Daten usw., sodass es im Grunde genommen die Funktionsweise des gesamten Speichers kopiert (vorausgesetzt, die Speicherzuweisung wird bereits von der Anwendung mithilfe erweiterter Algorithmen optimiert und verwaltet) und speichern sie in einem Stapel.Wenn die Kopie des Speichers angefordert wird, wird die Baumstruktur entsprechend der Notwendigkeit einer flachen oder tiefen Kopie kopiert.Eine tiefe Kopie wird nur für die Variable erstellt, die geändert wird.Da jede Variable mithilfe einer benutzerdefinierten Zuweisung zugewiesen wird, hat die Anwendung das letzte Wort, wann sie bei Bedarf gelöscht werden muss.Sehr interessant wird es, wenn wir das Rückgängigmachen/Wiederherstellen unterteilen müssen, wenn wir eine Reihe von Vorgängen programmgesteuert selektiv rückgängig machen/wiederholen müssen.In diesem Fall erhalten nur diese neuen Variablen oder gelöschten Variablen oder modifizierten Variablen eine Flagge, so dass diese Speicher Dinge rückgängig machen/wiederholen.Wenn dies der Fall ist, wird eine neuere Idee des „Besuchermusters“ verwendet.Es heißt „Rückgängigmachen/Wiederherstellen auf Objektebene“.

  1. Rückgängigmachen/Wiederherstellen auf Objektebene:Wenn die Benachrichtigung zum Rückgängigmachen/Wiederherstellen aufgerufen wird, implementiert jedes Objekt einen Streaming-Vorgang, bei dem der Streamer vom Objekt die alten Daten/neuen Daten erhält, die programmiert sind.Die Daten, die nicht gestört werden sollen, bleiben ungestört.Jedes Objekt erhält einen Streamer als Argument und streamt/entstreamt innerhalb des UNDo/Redo-Aufrufs die Daten des Objekts.

Sowohl 1 als auch 2 könnten Methoden wie 1 haben.Vorundo () 2.Afterundo () 3.Bewheredo () 4.AfterRedo().Diese Methoden müssen im Basisbefehl „Rückgängig/Wiederherstellen“ (nicht im Kontextbefehl) veröffentlicht werden, damit alle Objekte diese Methoden ebenfalls implementieren, um eine bestimmte Aktion zu erhalten.

Eine gute Strategie besteht darin, einen Hybrid aus 1 und 2 zu erstellen.Das Schöne ist, dass diese Methoden (1 und 2) selbst Befehlsmuster verwenden

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top