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