Undo-System mit dem Command Pattern: Sauberes Rückgängig-Machen für bessere UX
Ein Undo-System (Rückgängig-Machen) ist eines der wichtigsten Features für die Benutzererfahrung (UX) in fast jeder Anwendung. Technisch gesehen basiert die sauberste Umsetzung fast immer auf dem Command Pattern (Befehlsmuster).
In diesem Tutorial lernst du, wie du ein robustes Undo/Redo-System entwirfst und implementierst.
1. Das Grundkonzept: Das Command Pattern
Stell dir vor, eine Aktion ist nicht einfach nur eine Code-Zeile, sondern ein Objekt. Dieses Objekt weiß zwei Dinge:
- Wie man die Aktion ausführt (
execute). - Wie man sie genau umkehrt (
undo).
2. Die Architektur
Wir benötigen drei Hauptkomponenten:
- Command Interface: Ein Standard für alle Befehle.
- Konkrete Commands: Die eigentliche Logik (z. B. "Text schreiben", "Farbe ändern").
- Command Manager (History): Der Verwalter, der die Stapel (Stacks) für Undo und Redo hält.
3. Die Implementierung (Beispiel in Python)
Wir bauen ein einfaches System für einen Text-Editor.
Schritt 1: Das Interface definieren
Jeder Befehl muss eine execute- und eine undo-Methode haben.
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
Schritt 2: Die History verwalten
Hier nutzen wir das LIFO-Prinzip (Last-In, First-Out) mit zwei Listen (Stacks).
class CommandManager:
def __init__(self):
self.undo_stack = []
self.redo_stack = []
def execute_command(self, command):
command.execute()
self.undo_stack.append(command)
# Sobald eine neue Aktion gemacht wird, wird der Redo-Speicher gelöscht
self.redo_stack.clear()
def undo(self):
if not self.undo_stack:
print("Nichts zum Rückgängigmachen!")
return
command = self.undo_stack.pop()
command.undo()
self.redo_stack.append(command)
def redo(self):
if not self.redo_stack:
print("Nichts zum Wiederholen!")
return
command = self.redo_stack.pop()
command.execute()
self.undo_stack.append(command)
Schritt 3: Ein konkreter Befehl
Nehmen wir an, wir wollen Text zu einem Dokument hinzufügen.
class Editor:
def __init__(self):
self.content = ""
def __str__(self):
return f"Editor Inhalt: '{self.content}'"
class AddTextCommand(Command):
def __init__(self, editor, text_to_add):
self.editor = editor
self.text_to_add = text_to_add
def execute(self):
self.editor.content += self.text_to_add
def undo(self):
# Wir entfernen genau die Länge des hinzugefügten Textes am Ende
length = len(self.text_to_add)
self.editor.content = self.editor.content[:-length]
4. Anwendung des Systems
Jetzt führen wir alles zusammen:
# Setup
my_editor = Editor()
manager = CommandManager()
# Aktion 1: "Hallo " schreiben
cmd1 = AddTextCommand(my_editor, "Hallo ")
manager.execute_command(cmd1)
# Aktion 2: "Welt!" schreiben
cmd2 = AddTextCommand(my_editor, "Welt!")
manager.execute_command(cmd2)
print(my_editor) # Output: Editor Inhalt: 'Hallo Welt!'
# Rückgängig machen
manager.undo()
print(my_editor) # Output: Editor Inhalt: 'Hallo '
# Wiederholen
manager.redo()
print(my_editor) # Output: Editor Inhalt: 'Hallo Welt!'
5. Wichtige Tipps für die Praxis
- Zustand vs. Differenz:
- Differenz (wie oben): Man speichert nur, was sich geändert hat. Das spart Speicherplatz, ist aber komplexer bei sehr komplizierten Aktionen.
- Snapshot (Memento Pattern): Man speichert eine Kopie des kompletten Zustands. Sicherer, verbraucht aber bei großen Daten (z.B. hochauflösende Bilder) viel RAM.
- Limitierung der History:
Ein Undo-Stack sollte nicht unendlich wachsen. Begrenze die Liste z. B. auf 50 oder 100 Einträge (
if len(self.undo_stack) > 100: self.undo_stack.pop(0)). - Serialisierung: Wenn du die "History" speichern willst (damit man nach einem Neustart der App noch Undo drücken kann), müssen deine Command-Objekte serialisierbar (z.B. in JSON umwandelbar) sein.
- Gruppierung von Befehlen (Macros):
Manchmal löst ein Klick viele kleine Aktionen aus. Erstelle dafür ein
MacroCommand, das eine Liste von Commands enthält und diese gesammelt in einemundo()-Schritt rückgängig macht.
Zusammenfassung
Für ein Undo-System speicherst du Aktionen nicht als Code-Fluss, sondern als Daten-Objekte auf einem Stapel. Das Command Pattern sorgt dafür, dass deine Logik sauber getrennt bleibt und du beliebig viele Schritte vor und zurück gehen kannst.