Das Memento Pattern

„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.

Schreibe einen Kommentar