menu

Kommando zurück: Das Memento-Entwurfsmuster

Annähernd jede moderne Applikation verfügt über eine Funktion, die letzten Arbeitsschritte rückgängig zu machen. Den meisten browserbasierten Anwendungen hingegen fehlt diese komfortable Möglichkeit zur Korrektur vorheriger Befehle. Dieser Artikel zeigt eine Möglichkeit auf, Undo-Mechanismen zumindest für objekt-orientierte Software vergleichsweise einfach zu implementieren.Diese Serie beschäftigt sich mit Entwurfsmustern in PHP. Die im folgenden benötigten Grundlagen und Termini wurden im ersten Teil der Serie (Ausgabe 01/04) behandelt und mit dem Singleton ein erstes Muster eingeführt. Der zweite Teil vertiefte die Grundlagen und stellte mit dem Adapter ein weiteres Muster vor. Der nunmehr dritte Teil behandelt einen Vertreter der objekt-basierten Verhaltensmuster: Das Memento.

vom 17. Oktober 2006

Die Problembeschreibung

Die meisten Benutzer schätzen bei komplexen Applikationen die Möglichkeit, ihre letzte Aktion rückgängig machen zu können oder einen Zwischenstand festhalten und wiederherstellen zu können. Eine Undo-Funktion ermöglicht es beispielsweise, einen Befehl zu korrigieren, der unerwartete Auswirkungen hatte. Aber auch Speicher-Mechanismen sind vielseitig verwendbar: So bieten manche Quellcode-Editoren etwa die Möglichkeit, den momentanen Status der Benutzeroberfläche zu speichern. Der sogenannte „Snapshot“ (Schnappschuss) enthält Informationen über die geöffneten Dateien, die Menüleisten, die Position von Fenstern und vieles mehr. In einer späteren Session kann dadurch genau dieser Zustand wiederhergestellt werden und die Arbeit an der unterbrochenen Stelle wiederaufgenommen werden.

Auf den ersten Blick bieten browserbasierte Anwendungen eine einfache Möglichkeit, zum letzen Arbeitsschritt zurückzukehren: Die Zurück-Taste. Spätestens beim zweiten Blick wird aber schnell klar, dass die Browser-History kein echtes Undo ist. Man kehrt zwar zur vorherigen Darstellung zurück, die getätigten Änderungen aber sind keinesfalls rückgängig gemacht worden. Bei einfachen Änderungsformularen hilft eventuell ein erneutes Absenden des Formulars mit den unveränderten Werten, gelöschte Einträge lassen sich damit nicht wieder herstellen. Ein richtiger Ansatz, eine solche Funktion zu implementieren, besteht darin, verschiedene Snapshots unserer Applikation (und aller verwalteten Daten) anzulegen und diese bei Bedarf wieder „einzuspielen“.

 

Die Problemlösung

Die Implementierung von Undo- oder Speicher-Mechanismen bei objekt-orientierter Software stellt einen Programmierer vor vielerlei Aufgaben. Unter anderem sind die inneren Zustände diverser Klassen-Exemplare (Objekten) zu speichern, denn diese bilden den Status der gesamten Applikation ab.

Aber auch Skripte, die nur teilweise von der Objekt-Orientierung Gebrauch machen, lassen sich mit einem Trick in Snapshots abbilden. Dabei hilft uns eine eigens für diese Situation gedachte Klasse. Man legt alle Skript-Variablen in diesem Exemplar ab, damit sie an einer Stelle gekapselt sind. Der Vorgang kann entweder als Exemplar-Methode realisiert sein (siehe Listing 1) oder überladene Klassen und deren __set() und __get() Methoden nutzen. (siehe Listing 2). Voraussetzung allerdings für das Überladen ist unter PHP4 eine installierte Overloading-Extension; PHP5 unterstützt Überladen Out-of-the-Box.

Die wünschenswerte Kapselung der Exemplare stellt uns vor das Problem, den inneren Zustand eines Objektes zu speichern, ohne Details über dessen Implementierung zu verraten. PHP beherrscht zwar die vollständige Serialisierung von Exemplaren, doch spätestens mit den in PHP5 eingeführten Sprach-Konstrukten private und protected, untergräbt die Serialisierung die Kapselung von Exemplaren. Diese Problematik umgehen wir mit den Einsatz des Memento-Musters. Ein Memento kann als Speicherstand verstanden werden. Dieser Speicherstand besitzt Kenntnis über die konkrete Implementierung des zu speichernden Zustands eines Exemplars, ist aber völlig passiv: Nur das Exemplar weist ihm einen Zustand zu oder fragt diesen wieder ab. Die API des Memento ist folgerichtig auch einzig auf das zu speichernde Exemplar ausgelegt, eine Kommunikation mit dem Skript ist nicht vorgesehen und auch nicht erwünscht. Das Skript kann aber ein vom Exemplar erzeugtes Memento vorhalten, ohne mit der Memento-API zu kommunizieren: Das Memento wird dazu beispielsweise in einer Session abgelegt. Soll es aber auch nach Ablauf der Session noch verfügbar sein, muss das Memento-Exemplar in eine speicherbare Form gebracht werden. Ein mit serialize() serialsierter String des Mementos kann leicht in einer Datei oder einer Datenbank gespeichert werden. Der String selbst gibt keine Auskunft über die Implementierung des Exemplars, er enthält ausschließlich benötigte Daten zum Wiederherstellen dessen Status’. Ein Zurückwandeln des Strings in ein Memento-Exemplar ist mit unserialize() jederzeit problemlos möglich.

Der Ablauf einer Exemplar-Externalisierung (Speicherung) sieht nun wie folgt aus:

  1. Das Skript erteilt dem Exemplar den Befehl, eine speicherbare Repräsentation (Memento) zu erzeugen.
  2. Das Exemplar erzeugt ein neues Exemplar der Memento-Klasse und weist dem Memento über dessen API einen Zustand zu.
  3. Das Exemplar liefert dem Skript eine Referenz auf das Memento (oder das Memento selbst) zurück, damit dieses im Bedarfsfalle so abgelegt werden kann, dass es auch nach Ablauf eines Skripts im Folgeskript noch zur Verfügung steht. In unserem Beispiel wird das Memento-Exemplar by-value zurückgeliefert.

Nach erfolgter Speicherung in einem Memento kann der Zustand eines Exemplars jederzeit wiederhergestellt werden:

  1. Das Skript erteilt dem Exemplar den Befehl, seinen im Memento gespeicherten Zustand wiederherzustellen.
  2. Das Exemplar fragt den Zustand des Mementos über dessen API ab und stellt daraus seinen eigenen Zustand wieder her.

Auf die Frage hin, wieso ein Exemplar nicht direkt serialisiert werden darf, weise ich erneut darauf hin, dass die Implementierungsdetails eines Exemplars dem Skript nicht bekannt sein sollten. Das Exemplar des späteren Beispiels enthält eine Variable namens $geheim. Der Wert dieser Variable wird zu keinem Zeitpunkt an das Memento übergeben und bleibt somit auch verborgen; er ist irrelevant für den Status des Exemplars. Das Ausblenden solcher Details ist der größte Vorteil des Memento-Musters, dadurch wird auch Speicherplatz gespart.

Ein weiterer Vorteil des Musters kommt zum Tragen, wenn man dem Memento zusätzliche Aufgaben überträgt. Damit verlässt man zwar die engen Grenzen des Lehrbuchs, gewinnt aber völlig neue Möglichkeiten hinzu. Denkbar wäre, das Memento beim Setzen sich selbst in einer Datei oder Datenbank speichern zu lassen. Beim Lesen des Mementos könnte ebenfalls automatisiert ein beliebiger Speicherstand aus der Datenbank geholt werden. Der Vorteil: Das Memento erledigt nun die komplexe Aufgabe der dauerhaften Speicherung eines Exemplar-Status und dessen Wiederherstellung. Das Skript beauftragt dies mit zwei einfachen Methoden-Aufrufen. Die hierfür notwendigen und im Hintergrund ablaufenden Vorgänge bleiben ihm verborgen.

 

Die Implementierung

Die Möglichkeiten der Objekt-Orientierung sind unter PHP beschränkt. Zwar bietet PHP5 eine deutlich erweiterte Unterstützung für objekt-orientiertes Programmieren, aber einige Merkmale anderer Hochsprachen fehlen doch. Das Lehrbuch spricht beim Memento-Muster von „breiten“ und „schmalen“ Schnittstellen. Dies bezieht sich auf die Idee, einem befreundeten Kommunikationspartner (dem Exemplar) eine umfangreichere API als einem fremden (dem Skript) offen zu legen. Dies ist unter PHP nicht ohne weiteres realisierbar, was beim Memento-Muster allerdings keinen gravierenden Nachteil darstellt. Der Programmierer sollte nur diszipliniert genug sein, die Zuständigkeiten bei der Implementierung zu beachten und nicht zu verletzen.

Wie in den Beispielen der bereits vorgestellten Entwurfsmustern, ist auch dieses Beispiel sehr einfach gehalten, wodurch leider ein Stück Aussagekraft verloren geht. Der Vorteil des Memento-Musters wächst mit der Komplexität der von ihm abgebildeten Exemplare. Wenn ein Exemplar mehrere Klassen repräsentiert und/oder auf anderen Exemplaren aufsetzt, läuft das Memento-Muster zur Hochform auf. Dies ist in folgendem Beispiel aber nicht der Fall, zu viel Code würde es erschweren, das Beziehungsgeflecht schnell zu erfassen. Dennoch lässt sich das Beispiel einfach auf komplexere Anwendungsfälle adaptieren.

Die hier vorgestellte Konstellation kennt drei Teilnehmer: Das Skript (Listing 3), ein Exemplar einer Counter-Klasse (Listing 4) und deren Memento (Listing 5). Das Skript verwendet das Array $snapshots um in ihm die Mementos des Counter-Exemplars abzulegen. Nachdem das Exemplar instanziert und sein Zustand mit einem Methodenaufruf modifiziert wurde, wird ein Memento angefordert und in $snapshots abgelegt. Das Memento ist Exemplar einer einfachen Klasse, welche lediglich zwei Methoden kennt: Eine, um Werte in im Memento-Exemplar abzulegen und eine, diese wieder abzufragen. Die Counter-Methode $counter->read() gibt einen Teil des inneren Counter-Zustands aus und dient zur Kontrolle der mit $counter->increaseBy($value) und $counter->decreaseBy($value) gemachten Modifikationen. Es folgen weitere Methodenaufrufe an das Counter-Exemplar, deren Auswirkung wieder in neuen Mementos festgehalten wird. Zum Wiederherstellen der Mementos existiert die Methode $counter->restoreMemento($memento). Der Parameter $memento ist hierbei optional: Fehlt dieser, wird das letzte Memento genutzt. Da zuvor aber mehrere Mementos angelegt wurden, übergibt array_pop($array) das jeweils letzte Element (bzw. Memento) aus $snapshots und entfernt es gleichzeitig aus dem Array. Danach wird jeweils per $counter->read() der Zustand des Counter-Exemplars kontrolliert. Die Ausgaben zeigen nun, dass das Counter-Exemplar nach jedem Aufruf der restoreMemento Methode chronologisch invers seine letzten Zustände wieder annimmt.
Die letzen beiden Zeilen des Skripts erfüllen keine für das Beispiel nötige Funktion mehr, zeigen aber einen einfachen und schnellen Weg, ein Memento in eine speicherbare Form zu bringen. Die Besonderheit des soeben vorgestellten Beispiels liegt darin, dass dem Skript statt einer Referenz auf das Memento eine Kopie zurückgeliefert wird. Da mit mehreren Mementos gearbeitet wird, würde eine erneute Anforderung eines Mementos vorherige überschreiben, alle Referenzen in $snapshots würden auf ein und dasselbe Memento verweisen.

 

Die Bewertung

Der Ansatz, den momentanen Status einer Applikation am inneren Zustand einiger benutzter Objekte festzumachen, ist praktikabel: Die Kapselung der Objekte bringt den Vorteil mit sich, dass auch alle zur Wiederherstellung des Status benötigte Daten gekapselt sind. Das Memento bietet eine Möglichkeit, den inneren Zustand der Objekte zu externalisieren, ohne deren Kapselung zu verletzten. Dies versetzt uns in die Lage, jeden beliebigen Zustand eines Objekts (und damit den Status der gesamten Applikation an sich) wiederherstellen zu können. Gerade bei komplexeren, mehrschrittigen Arbeitsabläufen ist dies ein großer Zugewinn an Usability, da unerwartete Resultate eines Befehls einfach rückgängig gemacht werden können.

Wie bei allen musterbasierten Entwürfen ist auch beim Memento der Overhead an Code und die damit einhergehenden Performance-Einbussen ein nicht zu vernachlässigender Nachteil. Es existiert jedoch keine sinnvolle Alternative, die eine objekt-orientierte Undo-Funktionalität mit gleichem, geringen Aufwand realisiert.
Eine Problematik, welche von diesem Artikel ausgeklammert wird, ist die Verwendung einer Datenbank: Einmal physisch gelöschte Datensätze lassen sich auch beim Rückgängigmachen eines Befehls nicht wiederherstellen. Ein möglicher Ansatz ist hier das Kennzeichnen von Datensätzen als gelöscht, etwa durch setzen einer Flag. Eine solche Kennzeichnung kann beim Undo leicht wieder aufgehoben werden.

Im Gegensatz zu vielen anderen Entwurfsmustern, ist speziell in Bezug auf PHP wenig bis gar keine Literatur über das Memento-Muster vorhanden. Andere, in PHP realisierte Ansätze als den meinigen sind selbst nach einiger gewissenhafter Recherche weder auf php::patterns [2], noch in Google zu finden. Leser, welche das Memento-Muster aus anderen Sprachen kennen, mögen mir deshalb bitte verzeihen, dass ich stellenweise recht eigene Wege bei der Implementierung des Beispiels eingeschlagen habe. Auch ist die Theorie für eine Adaption in PHP ausgelegt und deshalb etwas vereinfacht dargestellt.

Abschließend betrachtet, stellt das Memento-Muster eine oftmals nicht ganz triviale, aber doch effektive Methode dar, den Benutzerkomfort einer Applikation durch Undo- und Speichermechanismen zu erhöhen.

Der nächste Teil der Serie stellt mit dem Beobachter (Observer) ein weiteres Entwurfsmuster vor. Es definiert Abhängigkeiten zwischen Exemplaren so, dass eine Änderung am Zustand eines Exemplars abhängige Exemplare benachrichtigt. Diese können sich daraufhin selbstständig aktualisieren. Im Bezug auf das Memento-Muster bedeutet dies, dass bei der Anforderung eines Mementos alle beteiligten Exemplare ihre Mementos aktualisieren und zur Verfügung stellen können.

 

Listing

class storage {
    var $storage = array();

    function store($index, $value) {
        $this->storage[$index] = $value;
    }

    function get($index) {
        return $this->storage[$index;
    }

}

$storage = new storage();
$storage->store(‘myVar’, 42);
echo $storage->get(‘myVar’); //gibt 42 aus

class storage {
    var $storage = array();

    function __get($index) {
        if(isset($this->storage[$index])) {
            return $this->storage[$index];
        } else {
            return FALSE;
        }
    }

    function __set($index, $value) {
        $this->storage[$index] = $value;
        return TRUE;
    }
}

class counter {
    var $counter = 0;
    var $memento = FALSE;
    var $geheimnis = 'streng geheim!';

    function increaseBy($value) {
        $this->counter += $value;
    }

    function decreaseBy($value) {
        $this->counter -= $value;
    }

    function read() {
        return $this->counter;
    }

    function createMemento() {
        if(!is_object($this->memento)) {
            require_once('class_memento.inc.php');
            $this->memento = new memento();
        }

        $this->memento->set('counter', $this->counter);
        return $this->memento;
    }

    function restoreMemento($memento = FALSE) {
        if(is_object($memento)) {
            $this->memento = $memento;
        } elseif(!is_object($this->memento)) {
            die('Kein Memento vorhanden!');
        }

        $this->counter = $this->memento->get('counter');    
        return TRUE;
    }
}

class memento {
    var $storage = array();

    function get($key) {
        return $this->storage[$key];
    }

    function set($key, $value) {
        $this->storage[$key] = $value;
    }
}

overload(‘storage’);
$storage = new storage();
$storage->myVar = 42;
echo $storage->myVar; //gibt 42 aus

require_once('class_counter.inc.php');
$counter = new counter();
$snapshots = array();

//Counter erhöhen und Memento speichern
$counter->increaseBy(10);
$snapshots[] = $counter->createMemento();
echo $counter->read(), "
\n"; //gibt 10 aus

//Counter verringern und Memento speichern
$counter->decreaseBy(5);
$snapshots[] = $counter->createMemento();
echo $counter->read(), "
\n"; //gibt 5 aus

//Counter erhöhen
$counter->increaseBy(10);
echo $counter->read(), "
\n"; //gibt 15 aus

//den letzen Arbeitsschritt rückgängig machen
$counter->restoreMemento(array_pop($snapshots));
echo $counter->read(), "
\n"; //gibt wieder 5 aus

//einen weiteren Arbeitsschritt rückgängig machen
$counter->restoreMemento(array_pop($snapshots));
echo $counter->read(), "
\n"; //gibt wieder 10 aus

//speicherbares Memento erstellen
$mementoString = serialize

 

 

Kommentar abgeben

Hinweis: Felder, welche mit einem Sternchen (*) gekennzeichnet sind, müssen ausgefüllt werden.