Das Visitor Pattern mit Default Methoden

„Besuche machen immer Freude – wenn nicht beim Kommen, so doch beim Gehen.“

Christian Morgenstern

Bei jedem Beitrag zum Visitor Pattern habe ich die Vermutung, es wird wohl der letzte zum Thema sein. Aber nur knapp drei Monate nach Kalenderspielereien mit Java – iCalendar folgt schon ein neuer Beitrag zu diesem Design-Pattern.

Das Prinzip hinter dem Visitor Pattern ist recht einfach. Eine Klasse kapselt eine Querschnittsfunktion für eine Reihe anderer Klassen. Damit die Bearbeitung für unterschiedliche Typen eine gewisse Varianz hat, existieren für diese unterschiedliche Methoden.

public interface Visitor<I, O> {
  O visitQuestion(QuestionNode node, I input);
  O visitResult(ResultNode node, I input);
}

Der hier dargestelle abstrakte Visitor definiert eine Methode für Instanzen des Typs QuestionNode und eine für Instanzen des Typs ResultNode. Über generische Typen für einen zusätzlichen Parameter und den Rückgabetyp ist der Visitor sehr vielseitig einsetzbar.

Eine mögliche Implementierung des Visitors ist hier skizziert. Der folgende TreeVisitor besucht QuestionNode und ResultNode Instanzen während der Auswertung eines Decision-Trees.

public class TreeVisitor implements Visitor<Context, Result> {
  public static final ResultNode ERROR_NODE = new ExceptionNode("error");

  @Override
  public Result visitQuestion(QuestionNode node, Context context) {
    Node next = node.getFollower(input, ERROR_NODE);
    return next.accept(this, context);
  }

  @Override
  public Result visitResult(ResultNode node, Context input) {
    return new Result(input.getHistory(), node.getId());
  }
}

In der Methode visitQuestion wird der nächste Knoten im Baum bestimmt. Da der aktuelle Knoten verschiedene Nachfolger hat, wird über die Daten im Context die Entscheidung getroffen. Danach wird auf dem nächsten Knoten die accept Methode aufgerufen.

Diese Methode ist das Herzstück des Design Pattern. Irgendeine Entscheidungsinstanz muss für einen Knoten wählen, welche Visitor Methode aufgerufen werden soll. Und diese Instanz ist der Knoten selbst.

public class SolutionNode implements ResultNode {
  public <I, O> O accept(Visitor<I, O> visitor, I input) {
    return visitor.visitResult(this, input);
  }
 
}

Der SolutionNode implementiert den ResultNode und muss deshalb auch die, vom Node Interface geerbte, accept Methode implementieren. Wie nicht anders zu vermuten, ruft der SolutionNode die visitResult Methode des Visitor und übergibt sich selbst an diese Methode.

public class ExceptionNode implements ResultNode {
  public <I, O> O accept(Visitor<I, O> visitor, I input) {
    return visitor.visitResult(this, input);
  }
  
}

Neben dem SolutionNode existieren weitere Implementierungen für das ResultNode Interface und auch für das QuestionNode Interface existieren verschiedene Implementierungen. Der ExceptionNode besitzt eine Implementierung der accept Methode, die identisch ist mit der aus dem ResultNode. Alle Implementierungen der accept Methode für einen Typ sind identisch. Das ist einleuchtend aber auch unästhetisch.

Seit Java 8 gibt es eine wunderbare Lösung um den redundanten Codes in jeder Implementierung zu umgehen. Dies sind die Default-Methoden in Interfaces. Statt also in allen ResultNode Implementierungen eine identische accept Methode einzufügen, kann diese in ResultNode bereitgestellt werden.

public interface ResultNode extends Node {
  default <I, O> O accept(Visitor<I, O> visitor, I input) {
    return visitor.visitResult(this, input);
  }
}

Neben der Reduzierung von redundanten Code wird auch die Implementierung des Visitor Pattern besser gekapselt. Das Wissen um das Pattern ist auf die Implementierung des Visitor und die zugehörigen Interfaces beschränkt. Das eine konkrete Klasse von einem Visitor besucht wird, bestimmt sich allein durch die Implementierung eines besuchbaren Interfaces ohne weiteren pattern-spezifischen Code.