„Why Are You Asking Me? I Can’t Remember What I’ve Done.“
Leonard Shelby
Bei der Entwicklung von Software handelt es sich immer um die Formulierung von Lösungen in Form von Software. Soll die Software eine gute Lösung für ein Problem sein, dann sind Design Pattern eine gute Hilfe. Design Pattern sind Musterlösungen für typische Probleme in der Softwareentwicklung. Wer sie kennt, entwickelt nicht nur Software, die eine etablierte Lösungsstrategie verfolgt, sondern erhöht auch den Wiedererkennungswert für Kollegen und reduziert die Entwicklungszeit.
Die bekanntesten Design Pattern entstammen dem gleichnamigen Buch der Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides). Auch das Memento Pattern, um das sich dieser Beitrag dreht, ist in diesem fast 30 Jahre altem Buch zu finden.
Für jedes Design Pattern gibt es ein entsprechendes Problem und beim Memento Pattern ist es die Frage, wie der Zustand eines Objekts zwischengespeichert werden kann, ohne seine interne Struktur zu entblößen.
Ein bekannter Anwendungsfall für das Memento Pattern ist der Undo-Mechanismus in vielen Anwendungen. Hat der Anwender irgendetwas geändert, beispielsweise einen Ahnen aus dem Stammbaum gelöscht, dann kann dies mit einem Undo rückgängig gemacht werden.
Der Undo-Mechanismus verwendet ein Liste von durchgeführten Änderungen. Jede neue Änderung wird an das Ende der Liste angehängt. Bei einem Undo wird das letzte Element der Liste genommen und die entsprechende Änderung zurückgenommen.
Manche Anwendungen beherrschen auch einen Redo-Mechanismus. Dann wird nicht das letzte Element aus der Liste genommen, sondern ein Zeiger auf das vorhergehende Element gesetzt. Jedes Undo verschiebt nur den Zeiger aber löscht kein Element. Soll dann ein Redo geschehen, dann wird der Zeiger in die andere Richtung verschoben. Die entsprechende Änderung wird durchgeführt und somit das vorherige Undo korrigiert.
Bei dem Memento Pattern gibt es drei zentrale Akteure. Zum einen den Originator dessen interner Zustand gespeichert werden soll, das Memento, dass den aktuellen internen Zustand speichert und den CareTaker, der eine Liste der Mementos hält.
Die Kapselung der internen Struktur bleibt bewahrt, weil das Memento keinen Zugriff auf seine interne Struktur gewährt. Die interne Struktur des Originator bleibt bewahrt, weil er das Memento erzeugt. Der CareTaker kennt nur die Reihenfolge in der die Mementos erzeugt wurden, der interne Zustand des Mementos oder dessen Originator sind ihm nicht bekannt.
Je nach Anwendungsfall ist die Methode zur Änderung des Zustands des Originators nur für die von ihm erzeugten Mementos zugänglich oder auch für andere Mementos.
Die oben dargestellte erste Variante erlaubt es Duplikate eines Originator zu erstellen, indem ein Memento auf ein andere Instanz der Originator Klasse angewendet wird. Dafür wird jedoch in Kauf genommen, das der Originator eine entsprechende Schnittstelle bereitstellen muss. Was da ist wird genutzt, und wenn auch nur zum Unfug treiben. Dies klingt wie der alte Wilhelm Busch, ist aber leitvolle Erfahrung bei der Bereitstellung einer jeder API.
Die zweite Variante kann das Wiederherstellen des Zustands des Originator allein über das Memento realisieren. In dem Fall muss der Initiator einer Zustandsänderung kein Wissen über den Originator haben. Der Zugriff auf den CareTaker und den von ihm verwalteten Mementos ist ausreichend.
Um das Design Pattern besser zu veranschaulichen, folgt ein Beispiel aus dem Ahnenforschungsbereich. Die Verarbeitung von Person
Instanzen soll mit dem Memento Pattern ausgerüstet werden. Zuerst einmal wird ein Memento
benötigt. Dafür wird ein gleichnamiges, generisches Interface bereitgestellt. Dann können neben Memento
Instanzen für Person
auch Instanzen für andere Typen implementiert werden.
public interface Memento<T> { void restore(); }
Ein weiteres Interface beschreibt einen generischen Originator
, der Memento
Instanzen für den entsprechenden Typ erzeugen kann.
public interface Originator<T> { Memento<T> createMemento(); }
Implementiert wird das Originator
Interface von der PersonImpl
Klasse und das Memento
Interface von einer privaten Nested Class PersonMemento
der PersonImpl
Klasse. In dieser Form kann von auserhalb nicht auf die PersonMemento
Instanz zugegriffen werden, sie hat jedoch Zugriff auf die Attribute der umgebenden PersonImpl
Klasse.
@Getter public class PersonImpl implements Originator<Person>, Person { private class PersonMemento implements Memento<Person> { private final String name; private final boolean privacyChecked; private PersonMemento(String name, boolean privacyChecked) { this.name = name; this.privacyChecked = privacyChecked; } @Override public void restore() { PersonImpl.this.name = name; PersonImpl.this.privacyChecked = privacyChecked; } } private String name; private boolean privacyChecked; @Override public Memento<Person> createMemento() { return new PersonMemento(name, privacyChecked); } @Override public void setName(String name) { this.name = name; } // ... }
In der createMemento
Methode wird der Konstruktor von PersonMemento
aufgerufen und die Attribute übergeben, die gesichert werden sollen. In der restore
Methode der Klasse PersonMemento
werden später, bei Bedarf, die Attribute wieder direkt in die Felder der PersonImpl
Klasse geschrieben. Im Beispiel ist zu sehen, dass auch das Attribut privacyChecked
verarbeitet werden kann, obwohl es von außen nicht zugreifbar ist.
Die CareTaker
Implementierung beinhaltet eine Liste aller Memento
Instanzen und eine Map
, in der die Memento
Instanzen pro Originator
abgespeichert sind. So können unterschiedliche Originator
unabhängig voneinander verändert werden.
public class CareTaker { private final List<Memento<?>> mementos = new ArrayList<>(); private final Map<Originator<?>, List<Memento<?>>> mementosByOriginator = new HashMap<>(); public List<Memento<?>> getMementos() { return mementos; } public List<Memento<?>> getPersonMementos(Originator<?> originator) { List<Memento<?>> result = mementosByPerson.get(person); return result == null ? List.of() : Collections.unmodifiableList(result); } public void add(Originator<?> originator) { Memento<?> memento = originator.createMemento(); mementosByOriginator.computeIfAbsent(originator, k -> new ArrayList<>()).add(memento); mementos.add(memento); } public void remove(Originator<?> originator) { List<Memento<?>> mementoList = mementosByPerson.remove(originator); if (mementoList != null) { mementos.removeAll(mementoList); } } }
Damit beide Collections passend befüllt und geleert werden, existieren eine add
und eine remove
Methode. Beide besitzen den Originator
als Parameter. In der add
Methode wird das Memento
erzeugt und in die Collections eingefügt. In der remove
Methode aus den Collections mittels des Originator
Parameters entfernt.
Das folgende Beispiel zeigt die Anwendung des Memento Patterns. Nachdem auf der PersonImpl
Instanz ein neuer Name gesetzt wird, wird ein Memento
im CareTaker
gespeichert.
CareTaker careTaker = new CareTaker(); PersonImpl person = new PersonImpl(); person.setName("Jens"); careTaker.add(person); person.setName("Hans"); careTaker.add(person); careTaker.getMementos().get(0).restore(); Assertions.assertEquals("Jens", person.getName()); careTaker.getMementos().get(1).restore(); Assertions.assertEquals("Hans", person.getName());
Nachteilig erscheint sofort, dass der CareTaker
immer zur Hand sein muss. Außerdem muss darauf geachtet werden, dass alle relevanten Stellen ein Memento
speichern. Der CareTaker
könnte direkt in die PersonImpl
eingefügt werden, dann wären beide Probleme behoben. In dem Fall würden aber auch Memento
Instanzen generiert, wenn der Einsatz des Memento Patterns nicht erwünscht ist.
Eine andere Lösung ist die Verwendung eines Decorators für die PersonImpl
Klasse. Sie ergänzt die PersonImpl
Implementierung um die Verwaltung der Memento
Instanzen im CareTaker
. Der CareTaker
wird im Konstruktor übergeben und dort auch eine Instanz von PersonImpl
erzeugt.
public class PersonMementoDecorator implements Person, Originator<Person>, AutoCloseable { private final CareTaker careTaker; private final PersonImpl person; public PersonMementoDecorator(CareTaker careTaker) { this.careTaker = Objects.requireNonNull(careTaker); this.person = new PersonImpl(); } @Override public String getName() { return person.getName(); } @Override public void setName(String name) { person.setName(name); careTaker.add(this); } @Override public boolean isPrivacyChecked() { return person.isPrivacyChecked(); } @Override public void close() { careTaker.remove(person); } @Override public Memento<Person> createMemento() { return person.createMemento(); } }
Das Setzen des Namens delegiert dies an die PersonImpl
Instanz und ruft dann die add
Methode des CareTakers
zum Speichern der Memento
Instanz auf. Alle Memento
Instanzen die der PersonMementoDecorator
Instanz zugeordnet sind, werden mit der close
Methode aus dem CareTaker
gelöscht. Da der Decorator auch das Interface AutoClosable
Implementiert, kann das obige Beispiel verkürzt formuliert werden.
CareTaker careTaker = new CareTaker(); try (PersonMementoDecorator person = new PersonMementoDecorator(careTaker)) { person.setName("Jens"); person.setName("Hans"); careTaker.getMementos().get(0).restore(); Assertions.assertEquals("Jens", person.getName()); careTaker.getMementos().get(1).restore(); Assertions.assertEquals("Hans", person.getName()); }
Die Implementierung mit dem PersonMementoDecorator
liefert Memento Unterstützung, wenn diese für die Arbeit mit Person
Instanzen notwendig ist und kann weggelassen werden bei allen Anwendungsfällen, bei denen Memento
Instanzen unnötig sind.