Samstag, 1. Mai 2010

Eine Alternative zum Besucher-Entwurfsmuster

Der Besucher ist ein unter Compilerbauern beliebtes Entwurfsmuster aus dem bekannten Buch der "Viererbande" [1]. Es hat jedoch einige schwerwiegende Nachteile. Meist kommt man mit einer ebenso naheliegenden wie praktischen Alternative zum Besucher-Entwurfsmuster aus, die ich hier vorstellen möchte.

Die Ausgangslage des Besucher-Entwurfsmusters lässt sich am besten am Beispiel eines Parsers verdeutlichen. Sobald der Quelltext eines Programms, also ein textförmiges, von Mensch und Maschine lesbares Dokument, vom Parser einmal in ein komplexes, strukturiertes Datenobjekt transformiert worden ist, sind verschiedene Weiterverarbeitungen möglich: Ein Compiler kann Bytecode erzeugen. Ein Interpreter könnte das Programm direkt ausführen. Ein Pretty Printer könnte eine formatierte Ausgabe des Programms erzeugen. Ein Code Inspector könnte Metriken errechnen oder problematische Anweisungen aufspüren. In allen Fällen wäre das grundsätzliche Vorgehen gleich: Die einzelnen Elemente der komplexen Objektstruktur werden nach einem bestimmten Verfahren durchlaufen, und für jedes Objekt wird eine - für die Klasse dieses Objekts jeweils passende - Operation ausgeführt. Ziel des Besucher-Entwurfsmusters ist, dieses Vorgehen zu vereinheitlichen und dabei die auszuführenden Operationen von den Klassen der Objektstruktur zu trennen.

Was sind die Teilnehmer des Entwurfsmusters "Besucher"?


  • Eine Objektstruktur (Baum, Liste, ...): Eine Sammlung von Objekten, nennen wir sie Elemente, die Instanzen einer überschaubar kleinen Menge von Klassen darstellen. Nennen wir diese Klassen Elementtypen.

  • Eine Operation, die auf dieser Objektstruktur ausgeführt werden soll, indem

  • ein Prozessor die Objektstruktur traversiert und für jedes durchlaufene Element eine je nach Elementtyp unterschiedliche Methode aufruft.



Das Ziel des Entwurfsmusters "Besucher" ist, den zur Operation gehörenden Code sowohl vom Prozessor als auch von den Elementtypen zu trennen und in einem eigenen Objekttyp zu kapseln: dem Besucher-Interface.

Das Besucher-Interface stellt für jeden Elementtyp eine Methode zur Verfügung. Alle diese Methoden können gleich benannt werden und unterscheiden sich nur im Typ ihres Arguments. Das ist so nur in Sprachen wie Java möglich, die das Überladen von Methoden zulassen, aber z.B. nicht in ABAP (dort müssen die Methoden dann eben pro Elementtyp unterschiedlich benannt werden).

Wenn wir als Objektstruktur zur Veranschaulichung das XML-DOM wählen, müsste die Besucherklasse zum Beispiel folgendes Set von Methoden anbieten:

interface Visitor {
public void visit( ElementNode n );
public void visit( TextNode n );
public void visit( CommentNode n);
...
}


Darüberhinaus stellt jede Elementklasse eine - in einer Abstraktion, z.B. einem Interface oder einer Oberklasse definierte - Methode accept( Visitor v) zur Verfügung. Der Prozessor ruft dann für jedes durchlaufene Element diese accept()-Methode auf, so dass auf dem Weg über das Element die jeweils passende Operation des Visitors ausgeführt wird.

Diese Notwendigkeit, die Objektstruktur um eine accept()-Methode zu erweitern, ist ein böser Nachteil des Besucher-Musters: Wir können nicht den Standard-XML-DOM verwenden, in Java etwa das Paket org.w3c.dom, sondern müssen jede Klasse des DOM durch eine selbstprogrammierte Klasse verschalen, um die neue Methode einführen zu können. Da ist es ein schwacher Trost, dass diese Methode wenigstens allgemein formuliert ist, so dass sie für die verschiedensten Aufgaben verwendet werden kann. Dass die in der Objektstruktur verwendeten konkreten Elementklassen die Methode accept() implementieren müssen, ist vielmehr ein klassischer Fall von "intrusion".

Auch muss man sich, wenn man das Prinzip Don't repeat yourself wirklich ernst nimmt, daran stossen, dass die Implementierungen pro Elementklasse den folgenden immer gleichen Delegations-Code enthalten (obwohl das natürlich für das Entwurfsmuster nicht zwingend ist):

class ElementNode implements Node {
...
public void accept( Visitor visitor) {
visitor.visit( this );
}
}


Die konkrete Implementierung ist nur da, damit der Überlade-Mechanismus anhand des Elementtyps die jeweils richtige visit()-Methode des Besuchers bindet. Wenn die Implementierung fehlte, würde das Überladen nicht korrekt funktionieren, da in Java die Bindung beim Überladen statisch, d.h. bereits zur Compilezeit erfolgt: Der Compiler kann und will gar nicht wissen, welcher konkreter Objekttyp zur Laufzeit tatsächlich beim Aufruf der visit(Node)-Methode übergeben wird.

Der Prozessor befiehlt nun den durchlaufenen Elementen, den Besucher zu akzeptieren. Nehmen wir einmal an, die Klasse DomTree implementiere einen Iterator, um ihre Elemente zu traversieren. Dann wird die Operation des konkreten Besuchers wie folgt ausgeführt:

void process( Visitor visitor, DomTree domTree)
for ( Node n: domTree ) {
n.accept( visitor );
}
}


Ein wesentlicher Zug des Besucher-Entwurfsmusters ist, das Besucherobjekt mit Hilfe der accept(Visitor)-Methode durch alle Elemente der Objektstruktur hindurchzuzwängen. Die auszuführenden Operationen werden gewissermassen von innen, von den Elementen der Objektstruktur aus aufgerufen. Ist das wirklich nötig, nur um eine Trennung des Operations- vom Elementcode zu erreichen?

Warum besteht überhaupt ein Interesse daran, den Besucher durch die Objekte hindurchzuschicken? Die Trennung der Concerns liesse sich auch mit einem viel einfacheren Entwurf erreichen, bei dem die Elemente nicht mit einer ihrer Logik fremden accept()-Methode verschmutzt werden, sondern so bleiben können, wie sie sind. Um im Bild zu bleiben: Das Besucher-Entwurfsmuster lässt den Besucher ins Wohnzimmer. Man kann es aber auch so einrichten, dass man den Besucher gar nicht hineinlässt, sondern bereits an der Haustür abfertigt! Man könnte dies als Hausierer-Entwurfsmuster bezeichnen.

Nehmen wir wieder zur Einfachheit an, unsere Objektstruktur sei ein DOM-Baum. Dann können wir neben dieser Objektstruktur als eigenständige Komponente einen TreeWalker ausmachen. Im TreeWalker ist festgelegt, wie der DOM-Baum traversiert wird. Da der TreeWalker dafür zuständig ist, die Elemente der Objektstruktur zu durchlaufen, ist es eine naheliegende Entscheidung, den Aufruf der visit()-Methode dem TreeWalker zu überlassen: Er sendet dem Visitor für jedes durchlaufene Element die visit()-Nachricht. Eine accept()-Methode wird dann gar nicht benötigt! Der TreeWalker könnte den DOM-Baum zum Beispiel mit der folgenden rekursiven Methode process(Node,Visitor) aufrufen:

import org.w3c.dom.*;
import org.w3c.dom.Node; // Der Name "Node" ist in Java nicht eindeutig
import static org.w3c.dom.Node.*; // Node-Konstanten

class TreeWalker {
public void process( Node n, Visitor v) {
NodeList children = n.getChildNodes();
int numberOfChildren = children.getLength();
for (int i = 0; i < numberOfChildren; ++i) {
process( children.item(i), v);
}
v.visit( n );
}
}


Bei diesem intuitiven Entwurf hat man alle drei Komponenten klar getrennt: Die Objektstruktur, das Durchlaufen der Teilobjekte und schliesslich den Besucher. Beim Durchlaufen der Objektstruktur werden die Teilobjekte der Reihe nach beschafft und dem Besucher zur Bearbeitung vorgelegt.

Es obliegt damit dem Visitor, was er mit den konkreten Node-Objekten macht, die der TreeWalker ihm bei seinem Gang durch den DOM-Baum vorlegt. Um die Operationen vom Typ des zur Laufzeit übergebenen Elements abhängig zu machen, muss irgendeine Art von Reflection eingesetzt werden: Abhängig vom Typ müssen jeweils unterschiedliche Methoden ausgeführt werden. In org.w3c.dom hat jeder Knoten einen bestimmten Knotentyp, der mit der Methode getNodeType() abgefragt werden kann. Damit ersparen wir es uns in diesem Fall, das Java Reflection API zu verwenden, um die aktuell vorliegende Elementklasse zu ermitteln.

Die Knotentypen sind kleine ganze Zahlen. Man kann sie in der Visitor-Implementierung als Index eines Arrays von Methodenreferenzen verwenden. Da es in Java keine Methodenreferenzen gibt, empfehlen sich anonyme innere Klassen. Der folgende Visitor gibt für Elemente den Elementnamen und für Textknoten den Textinhalt in der Konsole aus:

class ExampleVisitor implements Visitor {
// Der folgende Array "visitorFor" dient uns als Dispatcher
private static Visitor[] visitorFor = new Visitor[10];
static {
visitorFor[ELEMENT_NODE] = new Visitor() {
public void visit( Node n ) {
System.out.println( "Element " + ((Element)n).getTagName() );
}
};
visitorFor[TEXT_NODE] = new Visitor() {
public void visit( Node n ) {
System.out.println( "Textknoten: " + n.getTextContent() );
}
};
// usw. für die anderen Knotentypen
// begrenzte Menge, die sich praktisch nicht mehr ändert
}

public void visit( Node n) {

// Dispatchen
visitorFor[n.getNodeType()].visit( n );

}

}


Der Vollständigkeit halber sei hier noch die Testklasse aufgeführt, mit der ich den ganzen Entwurf ausprobiert habe. Sie baut ein Beispieldokument auf und übergibt dessen Wurzelelement und die Besucherinstanz an den TreeWalker

class Test {
public static void main( String[] args ) throws Exception {

// XML-Dokument aus einem Teststring einlesen
Document testDoc = getDocumentFromString(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"+
"<a>b<c><d/>e</c></a>" );

// Alle Objekte mit Hilfe des TreeWalkers besuchen
new TreeWalker().process(
// 1.Argument: Wurzelknoten des XML-Dokuments
testDoc.getDocumentElement(),
// 2.Argument: Besucherobjekt
new ExampleVisitor() );

}

private static Document getDocumentFromString( String doc )
throws Exception {
return
javax.xml.parsers.DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(
new org.xml.sax.InputSource(
new java.io.StringReader( doc ) ) );
}
}


Die Testklasse gibt für das Beispieldokument <a>b<c><d/>e</c></a> das erwartete Ergebnis aus:
Textknoten: b
Element d
Textknoten: e
Element c
Element a



Gehen wir einmal die in [1] aufgeführten Konsequenzen des Entwurfsmusters Besucher durch und vergleichen sie mit dem hier präsentierten Entwurf:


  • Besucher machen das Hinzufügen neuer Operationen einfach. Das ist hier genauso: In der Tat müssen weder die Elementtypen noch der TreeWalker geändert werden, wenn ein neues Visitor-Objekt entwickelt wird.

  • Ein Besucher führt verwandte Operationen zusammen und trennt sie von Operationen, die nichts mit der Aufgabe des Besuchers zu tun haben. Ist hier offensichtlich ebenfalls erfüllt.

  • Das Hinzufügen neuer Elementklassen ist schwer. Bei uns ist es leichter als im Besucher-Muster. Unsere Visitor-Schnittstelle enthält ja nur eine Methode. Bei Hinzufügen neuer Elementklassen bleibt diese Methode gültig, da sie mit der allgemeinen Elementschnittstelle definiert ist. Der für eine bestimmte Operation auszuführende Code muss natürlich implementiert werden. Der Dispatchmechanismus könnte jedoch leicht so formuliert werden, dass für unbekannte Elementklassen einfach gar keine Aktion ausgeführt wird.

  • Klassenhierarchieübergreifende Besucher. Hier wird festgestellt, dass die besuchten Elemente im Besuchermuster nicht unbedingt von einer gemeinsamen Oberklasse abgeleitet werden müssen. Das ist zwar richtig. Dafür müssen sie aber alle die accept(Visitor)-Methode in ihrer öffentlichen Schnittstelle enthalten. Sie haben also nicht notwendig eine gemeinsame Oberklasse, wohl aber ein gemeinsames Interface. Das schränkt die Verwendbarkeit des Besucher-Musters ein. Für den hier vorgestellten Entwurf gibt es dagegen überhaupt keine Einschränkungen an die Elementklassen. Auch wenn im DOM-Beispiel alle Elementklassen von Node erben, so ist das keineswegs nötig. Die gemeinsame Oberklasse könnte auch Object sein. Die Existenz einer gemeinsamen Element-Schnittstelle ermöglicht es den Besuchern allerdings, ihre Arbeit effizienter auszuführen (weil sonst mit Reflection gearbeitet werden muss).

  • Ansammeln von Zustandsinformationen. Der Besucher kann, während er die Knoten besucht, in seinen Attributen Informationen sammeln. Das ist hier gleichermassen möglich.

  • Aufbrechen der Kapselung. Da der Besucher nur auf die öffentlichen Komponenten der Elementklasse zugreifen kann, kann es vorkommen, dass man gezwungen ist, interne Informationen des Objekts zu veröffentlichen, um dem Besucher seine Arbeit zu ermöglichen. Das ist eine Konsequenz, die in dieser Form für alle Client-Server-Beziehungen von Objekten gilt, ganz unabhängig vom Besuchermuster.



Es gibt also keine Notwendigkeit, den Besucher mit einer accept(Visitor)-Schnittstelle durch die Objekte der Objektstruktur hindurchzuschicken. Es gibt einfachere und naheliegendere Entwürfe, die das Gleiche leisten wie das Besucher-Muster.

[1] Gamma, E; Helm, R.; Johnson, R.; Vlissides, J.: Entwurfsmuster.- Elemente wiederverwendbarer objektorientierter Software, Addison-Wesley Germany 1996.

1 Kommentar :

Frakturfreund hat gesagt…
Dieser Kommentar wurde vom Autor entfernt.