Discussion:
Gedanken zu Undo/Redo...
(zu alt für eine Antwort)
Daniel Bleisteiner
2003-10-23 06:39:15 UTC
Permalink
Hallo ihr!

Ich habe mir heute mal ein paar Gedanken gemacht, wie ich allgemein in
einer Applikation ein Undo/Redo realiseren kann - möglichst mir variabler
Tiefe. Dabei sollte natürlich möglichst wenig Speicher gefressen werden
und nur das jeweils notwendige behalten werden. Ausserdem soll der Ansatz
nahtlos in alle möglichen Projekte integrierbar sein. Ich stelle mir das
im Moment wie folgt vor und möchte bei dieser Gelegenheit nach weiteren
Gedanken von Euch Ausschau halten, die das ganze vielleicht ergänzen oder
verbessern.

Zuerst mal plane ich einen UndoManager, welcher per SingletonPattern nur
eine einzige Instanz pro Umgebung zuläßt. Dadurch habe ich die
Möglichkeit, auf den UndoManager zuzugreifen, ohne mir Gedanken darüber
machen zu müssen, ob die laufende Applikation diesen eigentlich nutzt oder
nicht. Falls sie es tut, werden meine Undo/Redo Objekte gespeichert. Falls
nicht, verschwende ich Speicher...

Ein Objekt, welches Undo unterstützt sollte überlicherweise ein kleines
Interface implementieren, welches Objekt#undo(UndoEvent ue) sowie
Objekt#redo(UndoEvent ue) definiert. Und da komme ich auch schon zum
nächsten wichtigen Punkt, das UndoEvent.

Darunter stelle ich mir eine Klasse vor, welche neben den Namen der Aktion
und dem Originator - also dem durchführenden Objekt - noch einen Hashtable
beinhaltet, in dem das Objekt (Originator) die für es selbst relevanten
Infos zu der Aktion ablegt. Mit Hilfe dieser Infos sollte das Objekt in
der Lage sein, die Aktion rückgängig zu machen oder auch abermals
auszuführen - falls notwendig mit entsprechenden Validierungen.

Wann immer das Objekt eine undo-würdige Aktion ausführt sollte diese
mittels UndoManager.getInstance().registerUndoEvent(UndoEvent ue) beim
Manager registriert werden. Dieser sammelt alle ankommenden Events in
einer Queue (Vector oder Array) und verwaltet dabei auch die Menge der
maximal vorzuhaltenden Events.

Die Applikation, welche den UndoManager nutzt, steuert diesen dann
wahlweise per Menü oder ToolBar und fordert den Manager dann im Falle des
Falles auf, den entsprechenden Schritt zurückzunehmen oder gleich 10
Schritte zurückzugehen. Der Manager sendet daraufhin die gesammelten
UndoEvents an die Originator unter Verwendung von undo() und redo() zurück
und diese führen die gewünschte Aktion aus.

Soweit sind meine Gedanken bisher gediehen! Was haltet ihr von diesem
Ansatz? Ist der realistisch oder habe ich etwas übersehen? Oder gibt es
vielleicht schon fertige Ansätze in dieser Richtung?

Ich freue mich über alle Anregungen von euch!
--
Daniel Bleisteiner

|NOTICE: The above email address has been temporary disabled
| because of the Win32/***@mm virus! I've got more
| then 500 infected mails per day!
Ilja Preuß
2003-10-23 07:20:03 UTC
Permalink
Schau Dir mal das Command pattern an - ist ziemlich genau das, was Du
vorhast.

Übrigens, redo und das ursprüngliche "do" sollten sich eigentlich nicht
unterscheiden, oder?

Gruß, Ilja
Daniel Bleisteiner
2003-10-23 07:50:30 UTC
Permalink
Post by Ilja Preuß
Schau Dir mal das Command pattern an - ist ziemlich genau das, was Du
vorhast.
Werd ich machen...
Post by Ilja Preuß
Übrigens, redo und das ursprüngliche "do" sollten sich eigentlich nicht
unterscheiden, oder?
Nicht wirklich... aber um das allgemein hinzubekommen muss ich zumindest
eine einheitliche Methode redo() per Interface definieren. Diese kann dann
natürlich je nach UndoEvent das passende aufrufen.
--
Daniel Bleisteiner

|NOTICE: The above email address has been temporary disabled
| because of the Win32/***@mm virus! I've got more
| then 500 infected mails per day!
Stefan Matthias Aust
2003-10-23 09:48:33 UTC
Permalink
Post by Ilja Preuß
Schau Dir mal das Command pattern an - ist ziemlich genau das, was Du
vorhast.
Übrigens, redo und das ursprüngliche "do" sollten sich eigentlich nicht
unterscheiden, oder?
Doch, das ist möglich. Ich würde

interface ICmd {
void doit();
void undo();
void redo();
}

vorsehen, denn das initiale "doit" (oder "run" oder "exec" oder so) muss
zunächst seinen Zustand aufzeichnen und alles Speichern, damit "undo"
möglich wird. Ein späteres "redo" muss das wahrscheinlich nicht mehr
machen.

bye
--
Stefan Matthias Aust // "Ist es normal, nur weil alle es tun?" -F4
Ilja Preuß
2003-10-24 07:22:39 UTC
Permalink
Post by Stefan Matthias Aust
Post by Ilja Preuß
Schau Dir mal das Command pattern an - ist ziemlich genau das, was Du
vorhast.
Übrigens, redo und das ursprüngliche "do" sollten sich eigentlich
nicht unterscheiden, oder?
Doch, das ist möglich. Ich würde
interface ICmd {
void doit();
void undo();
void redo();
}
vorsehen, denn das initiale "doit" (oder "run" oder "exec" oder so)
muss zunächst seinen Zustand aufzeichnen und alles Speichern, damit
"undo" möglich wird. Ein späteres "redo" muss das wahrscheinlich
nicht mehr machen.
Habe ich Schwierigkeiten, mir vorzustellen. Hast Du dafür ein Beispiel
parat?

Gruß, Ilja
Stefan Matthias Aust
2003-10-24 09:49:05 UTC
Permalink
Post by Ilja Preuß
Habe ich Schwierigkeiten, mir vorzustellen. Hast Du dafür ein Beispiel
parat?
Weis nicht sorecht, ob das überzeugt: Immer wenn externe Ressourcen im
Spiel sind, die man nicht jedes Mal anlegen will, wenn also eine
einmalige Initialisierung stattfinden muss. Man muss außerdem noch ein
dispose() haben, um diese Ressourcen wieder freizugeben. Das vergaß ich
letztes Mal.

class DeleteFileCmd implements ICmd {
String filename;
DeleteFileCmd(String filename) { this.filename = filename; }
void doit() {
deleteFile(filenameInGarbageBin(filename));
copyFile(filename, filenameInGarbageBin(filename));
deleteFile(filename);
}
void undo() {
copyFile(filenameInGarbageBin(filename), filename);
}
void redo() {
deleteFile(filename);
}
void dispose() {
deleteFile(filenameInGarbageBin(filename));
}
}

Man könnte doit() wohl so aufteilen:

doit() {
init();
redo();
}

Könnte man init() auch in den Konstruktor ziehen? Ungerne, denn es
erscheint mir sinnvoll, Befehle schon anzulegen, noch bevor sie jemals
ausgeführt werden. Man braucht sie, um z.B. enablement zu prüfen. Ich
lege mir ein Befehlsobjekt an und lasse es entscheiden, ob es wohl auf
der aktuellen Selektion arbeiten könnte. Nur die willigen Befehle werden
jetzt in ein Menü übernommen oder im Menü enabled. Dafür will ich
natürlich nicht schon umfangreiche Initialisierungen durchführen wie
etwa das Kopieren von Dateien.

bye
--
Stefan Matthias Aust // "Ist es normal, nur weil alle es tun?" -F4
Tobias Vogele
2003-10-23 09:37:31 UTC
Permalink
Hallo,
Post by Daniel Bleisteiner
Zuerst mal plane ich einen UndoManager, welcher per SingletonPattern nur
eine einzige Instanz pro Umgebung zuläßt.
Kennst Du schon die Klasse javax.swing.undo.UndoManager, die ist genau dafür
da, eine bestimmte Anzahl an javax.swing.undo.UndoableEdit-Objekten zu
speichern? Wenn nich, schau sie Dir doch mal an.
Post by Daniel Bleisteiner
Ein Objekt, welches Undo unterstützt sollte überlicherweise ein kleines
Interface implementieren, welches Objekt#undo(UndoEvent ue) sowie
Objekt#redo(UndoEvent ue) definiert. Und da komme ich auch schon zum
nächsten wichtigen Punkt, das UndoEvent.
Das Interface javax.swing.undo.UndoableEdit hat unter anderem dann die
Methoden undo() und redo().

Mir ist nicht ganz klar, warum Du das aufteilen willst in das Interface mit
undo und redo und das Event-Objekt. Eigentlich würde es doch reichen, wenn
die Aktion selbst weiß, wie sie sich rückgängig macht. So ist das wohl auch
in dem JDK-undo-Klassen vorgesehen, und ich fand das ganz passend, als ich
es mal benutzt habe.


Ich bin zur Zeit eher am Überleben, ob man wirklich den UndoManager als
Singelton will, oder ob es nicht vielleicht besser ist, verschiedene Ebenen
von Undo-Aktionen zu haben. Z.B. habe ich ein Programm, bei dem
Daten-Objekte eingegeben werden, z.B. Adressen. Da hätte ich in dem
Editier-Dialog in den Textfeldern vielleicht gerne buchstabenweises Undo,
aber wenn der Dialog bestätigt ist, seh ich im Hauptfenster eine Liste
aller eingegeben Adressen, da hätte ich dann vielleicht gerne ein Undo auf
der Ebene, daß man das komplette Löschen oder Ändern von ganzen Adressen
rückgängig macht. Ist sowas sinnvoll? Nimmt man da für jedes Fenster einen
eigenen UndoManager oder werden die Buchstaben-undo-Aktionen nach dem
Schließen des Dialogs einfach aus dem globalen Undo-manager entfernt?

Grüße,

tobi
--
URL: http://www.wartmal.de Email: ***@wartmal.de
Stefan Matthias Aust
2003-10-23 09:55:02 UTC
Permalink
Post by Tobias Vogele
Ich bin zur Zeit eher am Überleben, ob man wirklich den UndoManager als
Singelton will, oder ob es nicht vielleicht besser ist, verschiedene Ebenen
von Undo-Aktionen zu haben.
Davon unbeeinflusst, sollte man eigentlich immer die Existenz von
Singletons anzweifeln, denn sie machen häufig mehr Ärger als sie nützen.
Zumindest sollte man sie von "außen" manipulieren und ggf. ändern
können, sonst baut man sich für Unittests prima Stolpersteine ein.
Desweiteren sollten globale singletons immer threadsafe sein. Was, wenn
man das System bzw. in einem Webcontainer benutzen will, wo auf einmal
mehrere Exemplare der Anwendung in verschiedenen Threads laufen? Ist da
ein globaler Singleton immer noch eine gute Idee?

PS: Ich würde die Manager pro Fenster, ggf. pro Editor und/oder
Eingabefeld schachteln.

bye
--
Stefan Matthias Aust // "Ist es normal, nur weil alle es tun?" -F4
Daniel Bleisteiner
2003-10-23 09:59:08 UTC
Permalink
Post by Tobias Vogele
Kennst Du schon die Klasse javax.swing.undo.UndoManager, die ist genau
Da schau ich mal rein...
Post by Tobias Vogele
Mir ist nicht ganz klar, warum Du das aufteilen willst in das Interface
mit undo und redo und das Event-Objekt. Eigentlich würde es doch
reichen, wenn die Aktion selbst weiß, wie sie sich rückgängig macht. So
ist das wohl auch in dem JDK-undo-Klassen vorgesehen, und ich fand das
ganz passend, als ich es mal benutzt habe.
Dann ist die "Aktion" in diesem Fall das "Event-Objekt" in meinem Fall.
Also ein beliebiges Objekt, welches das undo/redo Interface unterstützt,
erzeugt AktionsObjekte die vom Typ javax.swing.undo.UndoableEdit sind -
richtig? Wenn ich das richtig verstehe, ist es also praktisch fast das
gleiche...

Ich schau wie gesagt mal rein... wenn das ganze meinen Gedanken
entspricht, kann ich es sicherlich einsetzen. Würde mir ein wenig Arbeit
sparen. Ich hatte heute früh einfach drauf los gesponnen und noch nicht
gross nachgeforscht (wie schreibt man "recharchiert"? ;).
--
Daniel Bleisteiner

|NOTICE: The above email address has been temporary disabled
| because of the Win32/***@mm virus! I've got more
| then 500 infected mails per day!
Ulrich Schramme
2003-10-23 11:29:23 UTC
Permalink
Post by Daniel Bleisteiner
(wie schreibt man "recharchiert"? ;).
"recherchiert" :-)
--
-- Ulli
www.u-schramme.de
Tobias Vogele
2003-10-23 13:45:59 UTC
Permalink
Hallo,
Post by Daniel Bleisteiner
Dann ist die "Aktion" in diesem Fall das "Event-Objekt" in meinem Fall.
Also ein beliebiges Objekt, welches das undo/redo Interface unterstützt,
erzeugt AktionsObjekte die vom Typ javax.swing.undo.UndoableEdit sind -
richtig? Wenn ich das richtig verstehe, ist es also praktisch fast das
gleiche...
Ich versteh nicht so ganz, wie Du das meinst. Wozu brauchst Du denn dann
überhaupt noch das andere Objekt, welches deine redo(Event) und
undo(Event)-Methoden hat?
Willst Du für jede undo-bare Aktion ein Objekt speichern, das bei undo
wiederum eine Aktion erzeugt, die dieses undo ausführen kann?

Grüße,

tobi
Daniel Bleisteiner
2003-10-23 14:10:33 UTC
Permalink
Post by Tobias Vogele
Ich versteh nicht so ganz, wie Du das meinst. Wozu brauchst Du denn dann
überhaupt noch das andere Objekt, welches deine redo(Event) und
undo(Event)-Methoden hat?
Vielleicht reden wir aneinander vorbei - ich bin mir auch nicht sicher.
Ich versuchs nochmal zu erklären.

Ein beliebiges Objekt führt verschiedene Aktionen aus, von denen einige
undo-fähig sind. Immer wenn eine solche undo-fähige Aktion durchgeführt
wird, erzeugt das Objekt eine Instanz des "UndoEvents", welche einen
Namen, eine Referenz auf das Objekt und alle zusätzlich notwendigen Daten
(in einem Hashtable) kapselt. Dieses Objekt wird an den UndoManager
geschickt, damit dieser dieses mit allen anderen sammelt. Der UndoManager
baut also einen Stack mit "UndoEvents" der verschiedensten Objekte auf.

Wenn ich dieses Objekt weglassen würde, könnte ich die Infos, welche
Aktion eigentlich ungeschehen gemacht werden soll, ja garnicht abrufen -
irgendwo muss ich sie ja ablegen...

Mir dämmerts... in deiner Vorstellung hat das "UndoEvent" selbst die undo
und redo Methoden - richtig? Warum eigentlich nicht... ich hatte diese
bisher bei dem Objekt gesehen, welches die Aktion ausgeführt hat, nicht im
von ihm erzeugten "UndoEvent". Ich hatte mir das so gedacht, dass der
UndoManager beim Objekt undo() mit dem "UndoEvent" aufruft...

Stimmt... das "UndoEvent" selbst weiss ja genausogut, was es zu tun hat...
oder ruft zur Not einfach die passende Methode vom Erzeuger auf... Okay,
damit fällt das Interface weg, ich brauche nur die "UndoEvents", welche
undo() und redo() kennen.

Gut, dass wir das geklärt haben ;)

Danke!
--
Daniel Bleisteiner

|NOTICE: The above email address has been temporary disabled
| because of the Win32/***@mm virus! I've got more
| then 500 infected mails per day!
Tobias Vogele
2003-10-23 14:25:58 UTC
Permalink
[lange Erklärung]
Ja, genau so hab ich's gemeint.
Gut, dass wir das geklärt haben ;)
Ja. ;-)

tobi
--
URL: http://www.wartmal.de Email: ***@wartmal.de
Stefan Matthias Aust
2003-10-23 15:11:30 UTC
Permalink
Post by Daniel Bleisteiner
Stimmt... das "UndoEvent" selbst weiss ja genausogut, was es zu tun
hat... oder ruft zur Not einfach die passende Methode vom Erzeuger
auf... Okay, damit fällt das Interface weg, ich brauche nur die
"UndoEvents", welche undo() und redo() kennen.
Gut, dass wir das geklärt haben ;)
Jetzt nenne deinen "Event" einfach nur noch "Command" und es passt auch
noch besser :) Unter Event würde man sich etwas anderes vorstellen.


bye
--
Stefan Matthias Aust // "Ist es normal, nur weil alle es tun?" -F4
Ilja Preuß
2003-10-24 07:25:28 UTC
Permalink
Post by Stefan Matthias Aust
Post by Daniel Bleisteiner
Stimmt... das "UndoEvent" selbst weiss ja genausogut, was es zu tun
hat... oder ruft zur Not einfach die passende Methode vom Erzeuger
auf... Okay, damit fällt das Interface weg, ich brauche nur die
"UndoEvents", welche undo() und redo() kennen.
Gut, dass wir das geklärt haben ;)
Jetzt nenne deinen "Event" einfach nur noch "Command" und es passt
auch noch besser :) Unter Event würde man sich etwas anderes
vorstellen.
*Und* packe das ursprüngliche "do" auch noch mit hinein - die
default-Implementierung von redo kann dann wahrscheinlich gleich daran
weiterdelegieren...

Gruß, Ilja
Mick Krippendorf
2003-10-24 08:46:46 UTC
Permalink
Post by Ilja Preuß
Post by Stefan Matthias Aust
Post by Daniel Bleisteiner
Gut, dass wir das geklärt haben ;)
Jetzt nenne deinen "Event" einfach nur noch "Command" und es passt
auch noch besser :) Unter Event würde man sich etwas anderes
vorstellen.
*Und* packe das ursprüngliche "do" auch noch mit hinein - die
default-Implementierung von redo kann dann wahrscheinlich gleich daran
weiterdelegieren...
Um es mal zu illustrieren:

<code>

interface UndoableCommand {
UndoableCommand execute();
}

class FooCommand implements UndoableCommand {
UndoableCommand execute() {
// do foo:
...
return new UndoableCommand() {
UndoableCommand execute() {
// undo foo:
...
// undoing undo of foo == doing foo:
return FooCommand.this;
}
}
}
}

</code>

Jetzt muss man bloß noch die von den diversen execute()s zurückgegebenen
UndoableCommands auf einen History-Stack packen.

Das ist natürlich nur ein grobes Schema, und lässt sich in verschiedene
Richtungen ausbauen, zB. so:

<code>

interface UndoableCommand {
void execute();
UndoableCommand undo();
}

class FooCommand implements UndoableCommand {
void execute() {
// do foo:
...
}
UndoableCommand undo() {
// undo foo:
...
return FooCommand.this;
}
}

</code>

Dabei merkt man sich einfach die UndoableCommands selbst.


Wahrscheinlich braucht man aber in jedem Fall einen zweiten Stack, in
dem man sich die undos der undos (= redos) merkt. Oder man verwendet
eine Liste mit einer Art Cursor, der dann hin und her wandern kann.
Falls dabei undos ausgeführt werden, und danach kein redo kommt, sondern
ein neuer Command, muss man natürlich alle übrigen redo-Objekte löschen.
(Wenn zB. die Liste von links nach rechts wächst, müssen alle Objekte
rechts vom Cursor gelöscht, und das neue Command-Objekt rechts anhängt
werden)

Man könnte auch eine Abfragemöglichkeit in die Command-Objekte einbauen,
durch die man erfahren kann, ob das Objekt überhaupt Undoable ist und
falls nicht, ob es ein implizites Commit auslöst (= die komplette
History löscht), zB. wenn es Daten wegschreibt, die nicht einfach
wieder ent-schrieben werden können, weil vielleicht schon jemand
anders sie inzwischen verwendet, oder ob man es einfach ignorieren kann,
weil es zB. in einem command-line-Programm bloß die Hilfe anzeigt. Dabei
ändert sich (hoffentlich) nichts am Zustand des Programms, und
ent-helfen ergibt auch irgendwie keinen Sinn...


Jedenfalls kann man so sozusagen die Lokalität der Daten der jeweiligen
do's, undo's und redo's maximieren, und das ist gut. Wenn etwa eine
Liste sortiert werden soll, ist es ziemlich unpraktisch, sie beim undo
zu ent-sortieren, statt dessen merkt man sich beim execute() des
SortCommands einfach eine Kopie der unsortierten Liste. Hier kann dann
der undo sein eigener undo sein (jetzt wieder gemäß dem ersten
Beispiel):

<code>

class SortCommand implements UndoableCommand {
MyList tmpList;
UndoableCommand execute() {
// create an exact copy of theMightyGlobalList in tmpList
// sort theMightyGlobalList
...
return new UndoableCommand() {
UndoableCommand execute() {
// swap tmpList and theMightyGlobalList
...
return this;
}
}
}
}

</code>


Gruß,
Mick.
Lesen Sie weiter auf narkive:
Loading...