Subsections of Entwurfsmuster

Visitor-Pattern

TL;DR

Häufig bietet es sich bei Datenstrukturen an, die Traversierung nicht direkt in den Klassen der Datenstrukturen zu implementieren, sondern in Hilfsklassen zu verlagern. Dies gilt vor allem dann, wenn die Datenstruktur aus mehreren Klassen besteht (etwa ein Baum mit verschiedenen Knotentypen) und/oder wenn man nicht nur eine Traversierungsart ermöglichen will oder/und wenn man immer wieder neue Arten der Traversierung ergänzen will. Das würde nämlich bedeuten, dass man für jede weitere Form der Traversierung in allen Klassen eine entsprechende neue Methode implementieren müsste.

Das Visitor-Pattern lagert die Traversierung in eigene Klassenstruktur aus.

Die Klassen der Datenstruktur bekommen nur noch eine accept()-Methode, in der ein Visitor übergeben wird und rufen auf diesem Visitor einfach dessen visit()-Methode auf (mit einer Referenz auf sich selbst als Argument).

Der Visitor hat für jede Klasse der Datenstruktur eine Überladung der visit()-Methode. In diesen kann er je nach Klasse die gewünschte Verarbeitung vornehmen. Üblicherweise gibt es ein Interface oder eine abstrakte Klasse für die Visitoren, von denen dann konkrete Visitoren ableiten.

Bei Elementen mit "Kindern" muss man sich entscheiden, wie die Traversierung implementiert werden soll. Man könnte in der accept()-Methode den Visitor an die Kinder weiter reichen (also auf den Kindern accept() mit dem Visitor aufrufen), bevor man die visit()-Methode des Visitors mit sich selbst als Referenz aufruft. Damit ist die Form der Traversierung in den Klassen der Datenstruktur fest verankert und über den Visitor findet "nur" noch eine unterschiedliche Form der Verarbeitung statt. Alternativ überlässt man es dem Visitor, die Traversierung durchzuführen: Hier muss in den visit()-Methoden für die einzelnen Elemente entsprechend auf mögliche Kinder reagiert werden.

In diesem Pattern findet ein sogenannter "Double-Dispatch" statt: Zur Laufzeit wird ein konkreter Visitor instantiiert und über accept() an ein Element der Datenstruktur übergeben. Dort ist zur Compile-Zeit aber nur der Obertyp der Visitoren bekannt, d.h. zur Laufzeit wird hier der konkrete Typ bestimmt und entsprechend die richtige visit()-Methode auf der "echten" Klasse des Visitors aufgerufen (erster Dispatch). Da im Visitor die visit()-Methoden für jeden Typ der Datenstrukur überladen sind, findet nun zur Laufzeit die Auflösung der korrekten Überladung statt (zweiter Dispatch).

Videos (HSBI-Medienportal)
Lernziele
  • (K2) Aufbau des Visitor-Patterns (Besucher-Entwurfsmusters)
  • (K3) Anwendung des Visitor-Patterns auf konkrete Beispiele, etwa den PM-Dungeon

Motivation: Parsen von "5*4+3"

Zum Parsen von Ausdrücken (Expressions) könnte man diese einfache Grammatik einsetzen. Ein Ausdruck ist dabei entweder ein einfacher Integer oder eine Addition oder Multiplikation zweier Ausdrücke.

expr : e1=expr '*' e2=expr      # MUL
     | e1=expr '+' e2=expr      # ADD
     | INT                      # NUM
     ;

Beim Parsen von "5*4+3" würde dabei der folgende Parsetree entstehen:

Strukturen für den Parsetree

Der Parsetree für diese einfache Grammatik ist ein Binärbaum. Die Regeln werden auf Knoten im Baum zurückgeführt. Es gibt Knoten mit zwei Kindknoten, und es gibt Knoten ohne Kindknoten ("Blätter").

Entsprechend kann man sich einfache Klassen definieren, die die verschiedenen Knoten in diesem Parsetree repräsentieren. Als Obertyp könnte es ein (noch leeres) Interface Expr geben.

public interface Expr {}

public class NumExpr implements Expr {
    private final int d;

    public NumExpr(int d) { this.d = d; }
}

public class MulExpr implements Expr {
    private final Expr e1;
    private final Expr e2;

    public MulExpr(Expr e1, Expr e2) {
        this.e1 = e1;  this.e2 = e2;
    }
}

public class AddExpr implements Expr {
    private final Expr e1;
    private final Expr e2;

    public AddExpr(Expr e1, Expr e2) {
        this.e1 = e1;  this.e2 = e2;
    }
}


public class DemoExpr {
    public static void main(final String... args) {
        // 5*4+3
        Expr e = new AddExpr(new MulExpr(new NumExpr(5), new NumExpr(4)), new NumExpr(3));
    }
}

Ergänzung I: Ausrechnen des Ausdrucks

Es wäre nun schön, wenn man mit dem Parsetree etwas anfangen könnte. Vielleicht möchte man den Ausdruck ausrechnen?

Zum Ausrechnen des Ausdrucks könnte man dem Interface eine eval()-Methode spendieren. Jeder Knoten kann für sich entscheiden, wie die entsprechende Operation ausgewertet werden soll: Bei einer NumExpr ist dies einfach der gespeicherte Wert, bei Addition oder Multiplikation entsprechend die Addition oder Multiplikation der Auswertungsergebnisse der beiden Kindknoten.

public interface Expr {
    int eval();
}

public class NumExpr implements Expr {
    private final int d;

    public NumExpr(int d) { this.d = d; }
    public int eval() { return d; }
}

public class MulExpr implements Expr {
    private final Expr e1;
    private final Expr e2;

    public MulExpr(Expr e1, Expr e2) {
        this.e1 = e1;  this.e2 = e2;
    }
    public int eval() { return e1.eval() * e2.eval(); }
}

public class AddExpr implements Expr {
    private final Expr e1;
    private final Expr e2;

    public AddExpr(Expr e1, Expr e2) {
        this.e1 = e1;  this.e2 = e2;
    }
    public int eval() { return e1.eval() + e2.eval(); }
}


public class DemoExpr {
    public static void main(final String... args) {
        // 5*4+3
        Expr e = new AddExpr(new MulExpr(new NumExpr(5), new NumExpr(4)), new NumExpr(3));

        int erg = e.eval();
    }
}

Ergänzung II: Pretty-Print des Ausdrucks

Nachdem das Ausrechnen so gut geklappt hat, will der Chef nun noch flink eine Funktion, mit der man den Ausdruck hübsch ausgeben kann:

Das fängt an, sich zu wiederholen. Wir implementieren immer wieder ähnliche Strukturen, mit denen wir diesen Parsetree traversieren ... Und wir müssen für jede Erweiterung immer alle Expression-Klassen anpassen!

Das geht besser.

Visitor-Pattern (Besucher-Entwurfsmuster)

Das Entwurfsmuster "Besucher" (Visitor Pattern) lagert die Aktion beim Besuchen eines Knotens in eine separate Klasse aus.

Dazu bekommt jeder Knoten im Baum eine neue Methode, die einen Besucher akzeptiert. Dieser Besucher kümmert sich dann um die entsprechende Verarbeitung des Knotens, also um das Auswerten oder Ausgeben im obigen Beispiel.

Die Besucher haben eine Methode, die für jeden zu bearbeitenden Knoten überladen wird. In dieser Methode findet dann die eigentliche Verarbeitung statt: Auswerten des Knotens oder Ausgeben des Knotens ...

public interface Expr {
    void accept(ExprVisitor v);
}

public class NumExpr implements Expr {
    private final int d;

    public NumExpr(int d) { this.d = d; }
    public int getValue() { return d; }

    public void accept(ExprVisitor v) { v.visit(this); }
}

public class MulExpr implements Expr {
    private final Expr e1;
    private final Expr e2;

    public MulExpr(Expr e1, Expr e2) {
        this.e1 = e1;  this.e2 = e2;
    }
    public Expr getE1() { return e1; }
    public Expr getE2() { return e2; }

    public void accept(ExprVisitor v) { v.visit(this); }
}

public class AddExpr implements Expr {
    private final Expr e1;
    private final Expr e2;

    public AddExpr(Expr e1, Expr e2) {
        this.e1 = e1;  this.e2 = e2;
    }
    public Expr getE1() { return e1; }
    public Expr getE2() { return e2; }

    public void accept(ExprVisitor v) { v.visit(this); }
}


public interface ExprVisitor {
    void visit(NumExpr e);
    void visit(MulExpr e);
    void visit(AddExpr e);
}

public class EvalVisitor implements ExprVisitor {
    private final Stack<Integer> erg = new Stack<>();

    public void visit(NumExpr e) { erg.push(e.getValue()); }
    public void visit(MulExpr e) {
        e.getE1().accept(this);  e.getE1().accept(this);
        erg.push(erg.pop() * erg.pop());
    }
    public void visit(AddExpr e) {
        e.getE1().accept(this);  e.getE1().accept(this);
        erg.push(erg.pop() + erg.pop());
    }
    public int getResult() { return erg.peek(); }
}

public class PrintVisitor implements ExprVisitor {
    private final Stack<String> erg = new Stack<>();

    public void visit(NumExpr e) { erg.push("NumExpr(" + e.getValue() + ")"); }
    public void visit(MulExpr e) {
        e.getE1().accept(this);  e.getE1().accept(this);
        erg.push("MulExpr(" + erg.pop() + ", " + erg.pop() + ")");
    }
    public void visit(AddExpr e) {
        e.getE1().accept(this);  e.getE1().accept(this);
        erg.push("AddExpr(" + erg.pop() + ", " + erg.pop() + ")");
    }
    public String getResult() { return erg.peek(); }
}


public class DemoExpr {
    public static void main(final String... args) {
        // 5*4+3
        Expr e = new AddExpr(new MulExpr(new NumExpr(5), new NumExpr(4)), new NumExpr(3));

        EvalVisitor v1 = new EvalVisitor();
        e.accept(v1);
        int erg = v1.getResult();

        PrintVisitor v2 = new PrintVisitor();
        e.accept(v2);
        String s = v2.getResult();
    }
}

Implementierungsdetail

In den beiden Klasse AddExpr und MulExpr müssen auch die beiden Kindknoten besucht werden, d.h. hier muss der Baum weiter traversiert werden.

Man kann sich überlegen, diese Traversierung in den Klassen AddExpr und MulExpr selbst anzustoßen.

Alternativ könnte auch der Visitor die Traversierung vornehmen. Gerade bei der Traversierung von Datenstrukturen ist diese Variante oft von Vorteil, da man hier unterschiedliche Traversierungsarten haben möchte (Breitensuche vs. Tiefensuche, Pre-Order vs. Inorder vs. Post-Order, ...) und diese elegant in den Visitor verlagern kann.

(Double-) Dispatch

Zur Laufzeit wird in accept() der Typ des Visitors aufgelöst und dann in visit() der Typ der zu besuchenden Klasse. Dies nennt man auch "Double-Dispatch".

Hinweis I

Man könnte versucht sein, die accept()-Methode aus den Knotenklassen in die gemeinsame Basisklasse zu verlagern: Statt

    public void accept(ExprVisitor v) {
        v.visit(this);
    }

in jeder Knotenklasse einzeln zu definieren, könnte man das doch einmalig in der Basisklasse definieren:

public abstract class Expr {
    /** Akzeptiere einen Visitor für die Verarbeitung */
    public void accept(ExprVisitor v) {
        v.visit(this);
    }
}

Dies wäre tatsächlich schön, weil man so Code-Duplizierung vermeiden könnte. Aber es funktioniert in Java leider nicht. (Warum?)

Hinweis II

Während die accept()-Methode nicht in die Basisklasse der besuchten Typen (im Bild oben die Klasse Elem bzw. im Beispiel oben die Klasse Expr) verlagert werden kann, kann man aber die visit()-Methoden im Interface Visitor durchaus als Default-Methoden im Interface implementieren.

Ausrechnen des Ausdrucks mit einem Visitor

Wrap-Up

Visitor-Pattern: Auslagern der Traversierung in eigene Klassenstruktur

  • Klassen der Datenstruktur

    • bekommen eine accept()-Methode für einen Visitor
    • rufen den Visitor mit sich selbst als Argument auf
  • Visitor

    • hat für jede Klasse eine Überladung der visit()-Methode
    • Rückgabewerte schwierig: Intern halten oder per return (dann aber unterschiedliche visit()-Methoden für die verschiedenen Rückgabetypen!)
  • (Double-) Dispatch: Zur Laufzeit wird in accept() der Typ des Visitors und in visit() der Typ der zu besuchenden Klasse aufgelöst

Challenges

Visitor-Pattern praktisch (und einfach)

Betrachten Sie den folgenden Code und erklären Sie das Ergebnis:

interface Fruit { }
class Apple implements Fruit { }
class Orange implements Fruit { }
class Banana implements Fruit { }
class Foo extends Apple { }

public class FruitBasketDirect {
    public static void main(String... args) {
        List<Fruit> basket = List.of(new Apple(), new Apple(), new Banana(), new Foo());

        int oranges = 0;  int apples = 0;  int bananas = 0;  int foo = 0;

        for (Fruit f : basket) {
            if (f instanceof Apple) apples++;
            if (f instanceof Orange) oranges++;
            if (f instanceof Banana) bananas++;
            if (f instanceof Foo) foo++;
        }
    }
}

Das Verwenden von instanceof ist unschön und fehleranfällig. Schreiben Sie den Code unter Einsatz des Visitor-Patterns um.

Diskutieren Sie Vor- und Nachteile des Visitor-Patterns.

Quellen
  • [Eilebrecht2013] Patterns kompakt
    Eilebrecht, K. und Starke, G., Springer, 2013. ISBN 978-3-6423-4718-4.
  • [Gamma2011] Design Patterns
    Gamma, E. und Helm, R. und Johnson, R. E. und Vlissides, J., Addison-Wesley, 2011. ISBN 978-0-2016-3361-0.

Observer-Pattern

TL;DR

Eine Reihe von Objekten möchte über eine Änderung in einem anderen ("zentralen") Objekt informiert werden. Dazu könnte das "zentrale" Objekt eine Zugriffsmethode anbieten, die die anderen Objekte regelmäßig abrufen ("pollen").

Mit dem Observer-Pattern kann man das aktive Polling vermeiden. Die interessierten Objekte "registrieren" sich beim "zentralen" Objekt. Sobald dieses eine Änderung erfährt oder Informationen bereitstehen o.ä., wird das "zentrale" Objekt alle registrierten Objekte über den Aufruf einer Methode benachrichtigen. Dazu müssen diese eine gemeinsame Schnittstelle implementieren.

Das "zentrale" Objekt, welches abgefragt wird, nennt man "Observable" oder "Subject". Die Objekte, die die Information abfragen möchten, nennt man "Observer".

Videos (HSBI-Medienportal)
Lernziele
  • (K2) Aufbau des Observer-Patterns (Beobachter-Entwurfsmusters)
  • (K3) Anwendung des Observer-Patterns auf konkrete Beispiele, etwa den PM-Dungeon

Verteilung der Prüfungsergebnisse

Die Studierenden möchten nach einer Prüfung wissen, ob für einen bestimmten Kurs die/ihre Prüfungsergebnisse im LSF bereit stehen.

Dazu modelliert man eine Klasse LSF und implementiert eine Abfragemethode, die dann alle Objekte regelmäßig aufrufen können. Dies sieht dann praktisch etwa so aus:

final Person[] persons = { new Lecturer("Frau Holle"),
                           new Student("Heinz"),
                           new Student("Karla"),
                           new Tutor("Kolja"),
                           new Student("Wuppie") };
final LSF lsf = new LSF();

for (Person p : persons) {
    lsf.getGradings(p, "My Module");   // ???!
}

Elegantere Lösung: Observer-Entwurfsmuster

Sie erstellen im LSF eine Methode register(), mit der sich interessierte Objekte beim LSF registrieren können.

Zur Benachrichtigung der registrierten Objekte brauchen diese eine geeignete Methode, die traditionell update() genannt wird.

Observer-Pattern verallgemeinert

Im vorigen Beispiel wurde die Methode update() einfach der gemeinsamen Basisklasse Person hinzugefügt. Normalerweise möchte man die Aspekte Person und Observer aber sauber trennen und definiert sich dazu ein separates Interface Observer mit der Methode update(), die dann alle "interessierten" Klassen (zusätzlich zur bestehenden Vererbungshierarchie) implementieren.

Die Klasse für das zu beobachtende Objekt benötigt dann eine Methode register(), mit der sich Observer registrieren können. Die Objektreferenzen werden dabei einfach einer internen Sammlung hinzugefügt.

Häufig findet sich dann noch eine Methode unregister(), mit der sich bereits registrierte Beobachter wieder abmelden können. Weiterhin findet man häufig eine Methode notifyObservers(), die man von außen auf dem beobachteten Objekt aufrufen kann und die dann auf allen registrierten Beobachtern deren Methoden update() aufruft. (Dieser Vorgang kann aber auch durch eine sonstige Zustandsänderung im beobachteten Objekt durchgeführt werden.)

In der Standarddefinition des Observer-Patterns nach [Gamma2011] werden beim Aufruf der Methode update() keine Werte an die Beobachter mitgegeben. Der Beobachter muss sich entsprechend eine eigene Referenz auf das beobachtete Objekt halten, um dort dann weitere Informationen erhalten zu können. Dies kann vereinfacht werden, indem das beobachtete Objekt beim Aufruf der update()-Methode die Informationen als Parameter mitgibt, beispielsweise eine Referenz auf sich selbst o.ä. ... Dies muss dann natürlich im Observer-Interface nachgezogen werden.

Hinweis: Es gibt in Swing bereits die Interfaces Observer und Observable, die aber als "deprecated" gekennzeichnet sind. Sinnvollerweise nutzen Sie nicht diese Interfaces aus Swing, sondern implementieren Ihre eigenen Interfaces, wenn Sie das Observer-Pattern einsetzen wollen!

Wrap-Up

Observer-Pattern: Benachrichtige registrierte Objekte über Statusänderungen

  • Interface Observer mit Methode update()
  • Interessierte Objekte
    1. implementieren das Interface Observer
    2. registrieren sich beim zu beobachtenden Objekt (Observable)
  • Beobachtetes Objekt ruft auf allen registrierten Objekten update() auf
  • update() kann auch Parameter haben
Challenges

Observer: Restaurant

Stellen Sie sich ein Restaurant vor, in welchem man nicht eine komplette Mahlzeit bestellt, sondern aus einzelnen Komponenten auswählen kann. Die Kunden bestellen also die gewünschten Komponenten, suchen sich einen Tisch und warten auf die Fertigstellung ihrer Bestellung. Da die Küche leider nur sehr klein ist, werden immer alle Bestellungen einer bestimmten Komponente zusammen bearbeitet - also beispielsweise werden alle bestellten Salate angerichtet oder die alle bestellten Pommes-Portionen zubereitet. Sobald eine solche Komponente fertig ist, werden alle Kunden aufgerufen, die diese Komponente bestellt haben ...

Modellieren Sie dies in Java. Nutzen Sie dazu das Observer-Pattern, welches Sie ggf. leicht anpassen müssen.

Observer: Einzel- und Großhandel

In den Vorgaben finden Sie ein Modell für eine Lieferkette zwischen Großhandel und Einzelhandel.

Wenn beim Einzelhändler eine Bestellung von einem Kunden eingeht (Einzelhandel#bestellen), speichert dieser den Auftrag zunächst in einer Liste ab. In regelmäßigen Abständen (Einzelhandel#loop) sendet der Einzelhändler die offenen Bestellungen an seinen Großhändler (Grosshandel#bestellen). Hat der Großhändler die benötigte Ware vorrätig, sendet er diese an den Einzelhändler (Einzelhandel#empfangen). Dieser kann dann den Auftrag gegenüber seinem Kunden erfüllen (keine Methode vorgesehen).

Anders als der Einzelhandel speichert der Großhandel keine Aufträge ab. Ist die benötigte Ware bei einer Bestellung also nicht oder nicht in ausreichender Zahl auf Lager, wird diese nicht geliefert und der Einzelhandel muss (später) eine neue Bestellung aufgeben.

Der Großhandel bekommt regelmäßig (Grosshandel#loop) neue Ware für die am wenigsten vorrätigen Positionen.

Im aktuellen Modell wird der Einzelhandel nicht über den neuen Lagerbestand des Großhändlers informiert und kann daher nur "zufällig" neue Bestellanfragen an den Großhändler senden.

Verbessern Sie das Modell, indem Sie das Observer-Pattern integrieren. Wer ist Observer? Wer ist Observable? Welche Informationen werden bei einem update mitgeliefert?

Bauen Sie in alle Aktionen vom Einzelhändler und vom Großhändler passendes Logging ein.

Anmerkung: Sie dürfen nur die Vorgaben-Klassen Einzelhandel und Grosshandel verändern, die anderen Vorgaben-Klassen dürfen Sie nicht bearbeiten. Sie können zusätzlich benötigte eigene Klassen/Interfaces implementieren.

Quellen

Template-Method-Pattern

TL;DR

Das Template-Method-Pattern ist ein Entwurfsmuster, bei dem ein gewisses Verhalten in einer Methode implementiert wird, die wie eine Schablone agiert, der sogenannten "Template-Methode". Darin werden dann u.a. Hilfsmethoden aufgerufen, die in der Basisklasse entweder als abstract markiert sind oder mit einem leeren Body implementiert sind ("Hook-Methoden"). Über diese Template-Methode legt also die Basisklasse ein gewisses Verhaltensschema fest ("Template") - daher auch der Name.

In den ableitenden Klassen werden dann die abstrakten Methoden und/oder die Hook-Methoden implementiert bzw. überschrieben und damit das Verhalten verfeinert.

Zur Laufzeit ruft man auf den Objekten die Template-Methode auf. Dabei wird von der Laufzeitumgebung der konkrete Typ der Objekte bestimmt (auch wenn man sie unter dem Typ der Oberklasse führt) und die am tiefsten in der Vererbungshierarchie implementierten Methoden aufgerufen. D.h. die Aufrufe der Hilfsmethoden in der Template-Methode führen zu den in der jeweiligen ableitenden Klasse implementierten Varianten.

Videos (YouTube)
Videos (HSBI-Medienportal)
Lernziele
  • (K3) Template-Method-Entwurfsmuster praktisch anwenden

Motivation: Syntax-Highlighting im Tokenizer

In einem Compiler ist meist der erste Arbeitsschritt, den Eingabestrom in einzelne Token aufzubrechen. Dies sind oft die verschiedenen Schlüsselwörter, Operationen, Namen von Variablen, Methoden, Klassen etc. ... Aus der Folge von Zeichen (also dem eingelesenen Programmcode) wird ein Strom von Token, mit dem die nächste Stufe im Compiler dann weiter arbeiten kann.

public class Lexer {
    private final List<Token> allToken;  // alle verfügbaren Token-Klassen

    public List<Token> tokenize(String string) {
        List<Token> result = new ArrayList<>();

        while (string.length() > 0) {
            for (Token t : allToken) {
                Token token = t.match(string);
                if (token != null) {
                    result.add(token);
                    string = string.substring(token.getContent().length(), string.length());
                }
            }
        }

        return result;
    }
}

Dazu prüft man jedes Token, ob es auf den aktuellen Anfang des Eingabestroms passt. Wenn ein Token passt, erzeugt man eine Instanz dieser Token-Klasse und speichert darin den gematchten Eingabeteil, den man dann vom Eingabestrom entfernt. Danach geht man in die Schleife und prüft wieder alle Token ... bis irgendwann der Eingabestrom leer ist und man den gesamten eingelesenen Programmcode in eine dazu passende Folge von Token umgewandelt hat.

Anmerkung: Abgesehen von fehlenden Javadoc etc. hat das obige Code-Beispiel mehrere Probleme: Man würde im realen Leben nicht mit String, sondern mit einem Zeichenstrom arbeiten. Außerdem fehlt noch eine Fehlerbehandlung, wenn nämlich keines der Token in der Liste allToken auf den aktuellen Anfang des Eingabestroms passt.

Token-Klassen mit formatiertem Inhalt

Um den eigenen Tokenizer besser testen zu können, wurde beschlossen, dass jedes Token seinen Inhalt als formatiertes HTML-Schnipsel zurückliefern soll. Damit kann man dann alle erkannten Token formatiert ausgeben und erhält eine Art Syntax-Highlighting für den eingelesenen Programmcode.

public abstract class Token {
    protected String content;

    abstract protected String getHtml();
}
public class KeyWord extends Token {
    @Override
    protected String getHtml() {
        return "<font color=\"red\"><b>" +  this.content + "</b></font>";
    }
}
public class StringContent extends Token {
    @Override
    protected String getHtml() {
        return "<font color=\"green\">" +  this.content + "</font>";
    }
}


Token t = new KeyWord();
LOG.info(t.getHtml());

In der ersten Umsetzung erhält die Basisklasse Token eine weitere abstrakte Methode, die jede Token-Klasse implementieren muss und in der die Token-Klassen einen String mit dem Token-Inhalt und einer Formatierung für HTML zurückgeben.

Dabei fällt auf, dass der Aufbau immer gleich ist: Es werden ein oder mehrere Tags zum Start der Format-Sequenz mit dem Token-Inhalt verbunden, gefolgt mit einem zum verwendeten startenden HTML-Format-Tag passenden End-Tag.

Auch wenn die Inhalte unterschiedlich sind, sieht das stark nach einer Verletzung von DRY aus ...

Don't call us, we'll call you

public abstract class Token {
    protected String content;

    public final String getHtml() {
        return htmlStart() + this.content + htmlEnd();
    }

    abstract protected String htmlStart();
    abstract protected String htmlEnd();
}
public class KeyWord extends Token {
    @Override protected String htmlStart() { return "<font color=\"red\"><b>"; }
    @Override protected String htmlEnd() { return "</b></font>"; }
}
public class StringContent extends Token {
    @Override protected String htmlStart() { return "<font color=\"green\">"; }
    @Override protected String htmlEnd() { return "</font>"; }
}


Token t = new KeyWord();
LOG.info(t.getHtml());

Wir können den Spaß einfach umdrehen ("inversion of control") und die Methode zum Zusammenbasteln des HTML-Strings bereits in der Basisklasse implementieren. Dazu "rufen" wir dort drei Hilfsmethoden auf, die die jeweiligen Bestandteile des Strings (Format-Start, Inhalt, Format-Ende) erzeugen und deren konkrete Implementierung wir in der Basisklasse nicht kennen. Dies ist dann Sache der ableitenden konkreten Token-Klassen.

Objekte vom Typ KeyWord sind dank der Vererbungsbeziehung auch Token (Vererbung: is-a-Beziehung). Wenn man nun auf einem Token t die Methode getHtml() aufruft, wird zur Laufzeit geprüft, welchen Typ t tatsächlich hat (im Beispiel KeyWord). Methodenaufrufe werden dann mit den am tiefsten in der vorliegenden Vererbungshierarchie implementierten Methoden durchgeführt: Hier wird also die von Token geerbte Methode getHtml() in KeyWord aufgerufen, die ihrerseits die Methoden htmlStart() und htmlEnd() aufruft. Diese sind in KeyWord implementiert und liefern nun die passenden Ergebnisse.

Die Methode getHtml() wird auch als "Template-Methode" bezeichnet. Die beiden darin aufgerufenen Methoden htmlStart() und htmlEnd() in Token werden auch als "Hilfsmethoden" (oder "Helper Methods") bezeichnet.

Dies ist ein Beispiel für das Template-Method-Pattern.

Template-Method-Pattern

Aufbau Template-Method-Pattern

In der Basisklasse implementiert man eine Template-Methode (in der Skizze templateMethod), die sich auf anderen in der Basisklasse deklarierten (Hilfs-) Methoden "abstützt" (diese also aufruft; in der Skizze method1, method2, method3). Diese Hilfsmethoden können als abstract markiert werden und müssen dann von den ableitenden Klassen implementiert werden (in der Skizze method1 und method2). Man kann aber auch einige/alle dieser aufgerufenen Hilfsmethoden in der Basisklasse implementieren (beispielsweise mit einem leeren Body - sogenannte "Hook"-Methoden) und die ableitenden Klassen können dann diese Methoden überschreiben und das Verhalten so neu formulieren (in der Skizze method3).

Damit werden Teile des Verhaltens an die ableitenden Klassen ausgelagert.

Verwandtschaft zum Strategy-Pattern

Das Template-Method-Pattern hat eine starke Verwandtschaft zum Strategy-Pattern.

Im Strategy-Pattern haben wir Verhalten komplett an andere Objekte delegiert, indem wir in einer Methode einfach die passende Methode auf dem übergebenen Strategie-Objekt aufgerufen haben.

Im Template-Method-Pattern nutzen wir statt Delegation die Mechanismen Vererbung und dynamische Polymorphie und definieren in der Basis-Klasse abstrakte oder Hook-Methoden, die wir bereits in der Template-Methode der Basis-Klasse aufrufen. Damit ist das grobe Verhalten in der Basis-Klasse festgelegt, wird aber in den ableitenden Klassen durch das dortige Definieren oder Überschreiben der Hilfsmethoden verfeinert. Zur Laufzeit werden dann durch die dynamische Polymorphie die tatsächlich implementierten Hilfsmethoden in den ableitenden Klassen aufgerufen. Damit lagert man im Template-Method-Pattern gewissermaßen nur Teile des Verhaltens an die ableitenden Klassen aus.

Wrap-Up

Template-Method-Pattern: Verhaltensänderung durch Vererbungsbeziehungen

  • Basis-Klasse:
    • Template-Methode, die Verhalten definiert und Hilfsmethoden aufruft
    • Hilfsmethoden: Abstrakte Methoden (oder "Hook": Basis-Implementierung)
  • Ableitende Klassen: Verfeinern Verhalten durch Implementieren der Hilfsmethoden
  • Zur Laufzeit: Dynamische Polymorphie: Aufruf der Template-Methode nutzt die im tatsächlichen Typ des Objekts implementierten Hilfsmethoden
Challenges

Schreiben Sie eine abstrakte Klasse Drucker. Implementieren Sie die Funktion kopieren, bei der zuerst die Funktion scannen und dann die Funktion drucken aufgerufen wird. Der Kopiervorgang ist für alle Druckertypen identisch, das Scannen und Drucken ist abhängig vom Druckertyp.

Implementieren Sie zusätzlich zwei unterschiedliche Druckertypen:

  • Tintendrucker extends Drucker
    • Tintendrucker#drucken loggt den Text "Drucke das Dokument auf dem Tintendrucker."
    • Tintendrucker#scannen loggt den Text "Scanne das Dokument mit dem Tintendrucker."
  • Laserdrucker extends Drucker
    • Laserdrucker#drucken loggt den Text "Drucke das Dokument auf dem Laserdrucker."
    • Laserdrucker#scannen loggt den Text "Scanne das Dokument mit dem Laserdrucker."

Nutzen Sie das Template-Method-Pattern.

Quellen
  • [Eilebrecht2013] Patterns kompakt
    Eilebrecht, K. und Starke, G., Springer, 2013. ISBN 978-3-6423-4718-4.
  • [Gamma2011] Design Patterns
    Gamma, E. und Helm, R. und Johnson, R. E. und Vlissides, J., Addison-Wesley, 2011. ISBN 978-0-2016-3361-0.

Command-Pattern

TL;DR

Das Command-Pattern ist die objektorientierte Antwort auf Callback-Funktionen: Man kapselt Befehle in einem Objekt.

  1. Die Command-Objekte haben eine Methode execute() und führen dabei Aktion auf einem bzw. "ihrem" Receiver aus.

  2. Receiver sind Objekte, auf denen Aktionen ausgeführt werden, im Dungeon könnten dies etwa Hero, Monster, ... sein. Receiver müssen keine der anderen Akteure in diesem Pattern kennen.

  3. Damit die Command-Objekte aufgerufen werden, gibt es einen Invoker, der Command-Objekte hat und zu gegebener Zeit auf diesen die Methode execute() aufruft. Der Invoker muss dabei die konkreten Kommandos und die Receiver nicht kennen (nur die Command-Schnittstelle).

  4. Zusätzlich gibt es einen Client, der die anderen Akteure kennt und alles zusammen baut.

Videos (YouTube)
Videos (HSBI-Medienportal)
Lernziele
  • (K2) Aufbau des Command-Patterns
  • (K3) Anwendung des Command-Patterns auf konkrete Beispiele, etwa den PM-Dungeon

Motivation

Irgendwo im Dungeon wird es ein Objekt einer Klasse ähnlich wie InputHandler geben mit einer Methode ähnlich zu handleInput():

public class InputHandler {
    public void handleInput() {
        switch (keyPressed()) {
            case BUTTON_W -> hero.jump();
            case BUTTON_A -> hero.moveX();
            case ...
            default -> { ... }
        }
    }
}

Diese Methode wird je Frame einmal aufgerufen, um auf eventuelle Benutzereingaben reagieren zu können. Je nach gedrücktem Button wird auf dem Hero eine bestimmte Aktion ausgeführt ...

Das funktioniert, ist aber recht unflexibel. Die Aktionen sind den Buttons fest zugeordnet und erlauben keinerlei Konfiguration.

Auflösen der starren Zuordnung über Zwischenobjekte

public interface Command { void execute(); }

public class Jump implements Command {
    private Entity e;
    public void execute() { e.jump(); }
}

public class InputHandler {
    private final Command wbutton = new Jump(hero);  // Über Ctor/Methoden setzen!
    private final Command abutton = new Move(hero);  // Über Ctor/Methoden setzen!

    public void handleInput() {
        switch (keyPressed()) {
            case BUTTON_W -> wbutton.execute();
            case BUTTON_A -> abutton.execute();
            case ...
            default -> { ... }
        }
    }
}

Die starre Zuordnung "Button : Aktion" wird aufgelöst und über Zwischenobjekte konfigurierbar gemacht.

Für die Zwischenobjekte wird ein Typ Command eingeführt, der nur eine execute()-Methode hat. Für jede gewünschte Aktion wird eine Klasse davon abgeleitet, diese Klassen können auch einen Zustand pflegen.

Den Buttons wird nun an geeigneter Stelle (Konstruktor, Methoden, ...) je ein Objekt der jeweiligen Command-Unterklassen zugeordnet. Wenn ein Button betätigt wird, wird auf dem Objekt die Methode execute() aufgerufen.

Damit die Kommandos nicht nur auf den Helden wirken können, kann man den Kommando-Objekten beispielsweise noch eine Entität mitgeben, auf der das Kommando ausgeführt werden soll. Im Beispiel oben wurde dafür der hero genutzt.

Command: Objektorientierte Antwort auf Callback-Funktionen

Im Command-Pattern gibt es vier beteiligte Parteien: Client, Receiver, Command und Invoker.

Ein Command ist die objektorientierte Abstraktion eines Befehls. Es hat möglicherweise einen Zustand, und und kennt "seinen" Receiver und kann beim Aufruf der execute()-Methode eine vorher verabredete Methode auf diesem Receiver-Objekt ausführen.

Ein Receiver ist eine Klasse, die Aktionen durchführen kann. Sie kennt die anderen Akteure nicht.

Der Invoker (manchmal auch "Caller" genannt) ist eine Klasse, die Commands aggregiert und die die Commandos "ausführt", indem hier die execute()-Methode aufgerufen wird. Diese Klasse kennt nur das Command-Interface und keine spezifischen Kommandos (also keine der Sub-Klassen). Es kann zusätzlich eine gewisse Buchführung übernehmen, etwa um eine Undo-Funktionalität zu realisieren.

Der Client ist ein Programmteil, der ein Command-Objekt aufbaut und dabei einen passenden Receiver übergibt und der das Command-Objekt dann zum Aufruf an den Invoker weiterreicht.

In unserem Beispiel lassen sich die einzelnen Teile so sortieren:

  • Client: Klasse InputHandler (erzeugt neue Command-Objekte im obigen Code) bzw. main(), wenn man die Command-Objekte dort erstellt und an den Konstruktor von InputHandler weiterreicht
  • Receiver: Objekt hero der Klasse Hero (auf diesem wird eine Aktion ausgeführt)
  • Command: Jump und Move
  • Invoker: InputHandler (in der Methode handleInput())

Undo

Wir könnten das Command-Interface um ein paar Methoden erweitern:

public interface Command {
    void execute();
    void undo();
    Command newCommand(Entity e);
}

Jetzt kann jedes Command-Objekt eine neue Instanz erzeugen mit der Entity, die dann dieses Kommando empfangen soll:

public class Move implements Command {
    private Entity e;
    private int x, y, oldX, oldY;

    public void execute() { oldX = e.getX();  oldY = e.getY();  x = oldX + 42;  y = oldY;  e.moveTo(x, y); }
    public void undo() { e.moveTo(oldX, oldY); }
    public Command newCommand(Entity e) { return new Move(e); }
}

public class InputHandler {
    private final Command wbutton;
    private final Command abutton;
    private final Stack<Command> s = new Stack<>();

    public void handleInput() {
        Entity e = getSelectedEntity();
        switch (keyPressed()) {
            case BUTTON_W -> { s.push(wbutton.newCommand(e)); s.peek().execute(); }
            case BUTTON_A -> { s.push(abutton.newCommand(e)); s.peek().execute(); }
            case BUTTON_U -> s.pop().undo();
            case ...
            default -> { ... }
        }
    }
}

Über den Konstruktor von InputHandler (im Beispiel nicht gezeigt) würde man wie vorher die Command-Objekte für die Buttons setzen. Es würde aber in jedem Aufruf von handleInput() abgefragt, was gerade die selektierte Entität ist und für diese eine neue Instanz des zur Tastatureingabe passenden Command-Objekts erzeugt. Dieses wird nun in einem Stack gespeichert und danach ausgeführt.

Wenn der Button "U" gedrückt wird, wird das letzte Command-Objekt aus dem Stack genommen (Achtung: Im echten Leben müsste man erst einmal schauen, ob hier noch was drin ist!) und auf diesem die Methode undo() aufgerufen. Für das Kommando Move ist hier skizziert, wie ein Undo aussehen könnte: Man muss einfach bei jedem execute() die alte Position der Entität speichern, dann kann man sie bei einem undo() wieder auf diese Position verschieben. Da für jeden Move ein neues Objekt angelegt wird und dieses nur einmal benutzt wird, braucht man keine weitere Buchhaltung ...

Wrap-Up

Command-Pattern: Kapsele Befehle in ein Objekt

  • Command-Objekte haben eine Methode execute() und führen darin Aktion auf Receiver aus
  • Receiver sind Objekte, auf denen Aktionen ausgeführt werden (Hero, Monster, ...)
  • Invoker hat Command-Objekte und ruft darauf execute() auf
  • Client kennt alle und baut alles zusammen

Objektorientierte Antwort auf Callback-Funktionen

Challenges

Schreiben Sie für den Dwarf in den Vorgaben einen Controller, welcher das Command-Pattern verwendet.

  • "W" führt Springen aus
  • "A" bewegt den Zwerg nach links
  • "D" bewegt den Zwerg nach rechts
  • "S" führt Ducken aus

Schreiben Sie zusätzlich für den Cursor einen Controller, welcher das Command-Pattern mit Historie erfüllt (ebenfalls über die Tasten "W", "A", "S" und "D").

Schreiben Sie eine Demo, um die Funktionalität Ihres Programmes zu demonstrieren.

Quellen