Das Besucher-Muster (Visitor Pattern) in C++Varianten der Implementierung in C++Das Besucher Muster - engl. Visitor Pattern und im folgenden Text einfach Visitor genannt - wurde in dem Buch „Design Patterns. Elements of Reusable Objectoriented Software.“1) von E. Gamma et. al. [GoF95] mit diesem Namen belegt. Dieses Muster gehört zu den trickreicheren des Musterkatalogs in der genannten Publikation und kann in unterschiedlichen Varianten verschiedene Anforderungen realisieren helfen. Zunächst aber zu der grundsätzlichen Funktionsweise des Visitors: Das PrinzipDas Muster definiert eine Art Infrastruktur für den Zugriff auf komplexe dynamische Daten- und Objektstrukturen. Dabei wird der dynamischen Struktur eine Methodenschnittstelle gegeben, die ein sogenanntes Visitorobjekt entgegennimmt und über alle Daten der Struktur leitet. Das Visitorobjekt läuft also an allen Elementen der Struktur vorbei und wird von den Elementen über eine Methode „akzeptiert“. Dabei ruft das Strukturelement eine Methode im Visitorobjekt auf, die speziell für den Typ des Strukturelements geschrieben wurde und eine Referenz oder einen Zeiger auf das Strukturelement in ihrer Parameterliste entgegennimmt. Damit hat der Visitor wiederum die Chance eine Operation auf das Strukturelement auszuführen und dabei auf dessen Typ zu reagieren.
Der Sinn des Visitors besteht darin, die eigentliche Operation auf die Daten von der Traversierung über die Verwaltungsstruktur zu trennen. Aus der Entkopplung dieser beiden Aspekte entstehen Freiheitsgrade für Variationen derselben. Die Traversierung wird durch die genannte Infrastruktur aus Methoden übernommen. Die durch den Visitor transportierte Operation wird in einer Kindklasse der Visitorschnittstelle implementiert. Damit lassen sich beliebige Operationen definieren und durch den allgemeinen Traversierungsmechanismus in die Objektstruktur tragen. Mit dem Namen des Visitors wird das Verhalten des Musters gewürdigt. Das Visitorobjekt geht die Objekte in der Objektstruktur „besuchen“.
Die Funktionsweise des Visitors beruht auf einer standardisierten Zusammenarbeit zwischen den
Objekten der traversierten Datenstruktur mit dem Visitorobjekt. Diese Zusammenarbeit wird durch die
Infrastrukturmethoden der Daten implementiert, die üblicherweise Durch die Anwendung des Visitors erspart man sich die wiederholte Implementierung der Traversierung. Die Berücksichtigung der Typen der Daten durch den Visitor liefert eine Vorlage zur bequemen Definition von sehr unterschiedlichen Operationen. Die Daten können bei der Implementierung der Operation unabhängig vor Ihrer Ordnungsstruktur betrachtet werden. Die Ordnungsstruktur muss nur dann mitbetrachtet werden, wenn eine spezielle Operation diese auch berücksichtigen muss.
Im Folgenden sollen schematische Implementierungen des Visitors gezeigt und diskutiert werden. Dabei sollen die Vor- und Nachteile des Visitors wie auch die Stärken und die Schwächen spezifischer Implementierungen in C++ beleuchtet werden. Dazu wird anhand einer schematischen Implementierung eines Kompositums eine Objektstruktur vorgegeben, die durch einen Visitor bearbeitet wird. Auch der Visitor wird schematisch implementiert, was die Diskussion am Muster ohne die Berücksichtigung externer fachlicher Aspekte erleichtert. Es wird dabei dem Leser überlassen, die Transferleistung in eine reale Projektumgebung durch die eigene Vorstellungskraft zu leisten. Das sollte aber nicht allzu schwer fallen, denn die schematischen Implementierungen lassen sich mit nur wenigen Änderungen in realer Software nutzbar machen. Eine praktische Demonstration
Die im Listing 1 vorgestellte schematische Kompositimplementierung steht für eine beliebige dynamische
Objekt- oder Datenstruktur. Sie wurde gewählt, um zu zeigen, dass sich der Visitor nicht nur auf
lineare oder flache Strukturen anwenden lässt. Er ist in der Lage beliebige Strukturen zu durchlaufen.
Das Kompositum stellt eine komplexe Baumform dar, die mit dem Visitor sehr gut bearbeitet werden kann.
Bevor auf besondere Vor- und Nachteile des Visitors eingegangen wird, soll er in einer ersten Variante
an dem Kompositum gezeigt werden.
Da es sich bei der Visitorschnittstelle um eine Klasse handelt die abgeleitet werden soll, braucht sie auch einen virtuellen Destruktor. Ausserdem müssen die Parametertypen vordeklariert werden, denn sie können bei der Definition der Visitorschnittstelle noch nicht bekannt sein. Im Listing 2 wird diese Vorwärtsdeklaration innerhalb der Parameterlisten angebracht. Später sollen von dieser Schnittstelle die Klassen abgeleitet werden, die die Operationen implementieren.
Zuerst aber muss die Datenstruktur die
Die Implementierung der
Nun kann man über die Visitorschnittstelle Operationen definieren. Dazu überlädt man
die Das erste Beispiel der Anwendung soll genau solche Zustandsinformationen über die Objektstruktur der Komposite sammeln und verfügbar machen. Der Testcode in Listing 4 soll einen ersten Baum aufbauen, der für einen Test dienen kann.
Der Testcode kann natürlich beliebig variiert werden. Die erste Implementierung einer Operation über die Visitorschnittstelle soll zeigen, dass der Entwickler einer solchen Operation die tatsächliche Struktur nicht mehr berücksichtigen muss.
Um eine Operation zu schreiben muss der Entwickler von der Schittstellenklasse
Die Zählerklasse aus Listing 5 enthält das Attribut
Dieser Aufruf von Dieses einfache Beispiel soll die grundsätzliche Funktionsweise des Visitors demonstrieren. Dabei wurde auf den Aspekt, dass der Visitor auf die unterschiedlichen Typen des Objektbaums reagieren kann noch gar nicht eingegangen. Eine einfache Variante der Zählerklasse zeigt diese Möglichkeit: in Listing 6 wurde die Zählerklasse so abgewandelt, dass sie die Anzahlen der Objekte unterschiedlicher Typen separat zählt. Die Berücksichtigung der Objekttypen durch den Visitor
Durch die minimalen Änderungen aus Listing 6 reagiert der Zähler bereits auf die
unterschiedlichen Typen im Objektbaum. Er hat einfach drei neue Attribute bekommen, die in den
jeweiligen
Bis jetzt wurden zwei Operationen demonstriert, die einen rein lesenden Zugriff auf die Objektstruktur
ausführen. Dabei wird noch nicht einmal irgend ein interner Objektstatus abgefragt, sondern nur
das Vorhandensein verschiedener Objekte vermerkt. Weitere Varianten in der ImplementierungNach diesem theoretischen Absatz soll nun wieder etwas praktischer Code folgen. In unserem Zähler ist es möglich, auf unterschiedliche Objekttypen zu reagieren. Was der unsere Beispielimplementierung des Besuchers bis jetzt nicht kann, ist auf die Verschachtelungstiefe des Objektbaumes reagieren. Das zugrunde liegende Problem ist, dass die bisher implementierte Infrastruktur nur auf Objekttypen und nicht auf Eigenheiten der Organisationsform sensibel ist. Beispielhaft an den Kompositdaten soll nun dem Visitor eine solche Sensibilität gegeben werden.
Neben den
Wenn sich nun eine Operation für die Verschachtelungstiefe des Objektbaums „interessiert“,
muss sie nur dementsprechend die Natürlich sehen solche Varianten, die die Organisationsform der Objektstruktur berücksichtigen sehr unterschiedlich aus, denn sie müssen spezifisch an die Struktur angepasst werden. Das Problem der LeerimplementierungenWenn in der Objektstruktur viele Typen vertreten sind, kann sich für die Implementierung der Operationen das Problem ergeben, dass viele Methoden wiederholt leer implementiert werden müssen. Das kann sehr unangenehm sein und im Extremfall kann der formale Aufwand die Vorteile des Visitors überwiegen. Zudem sind viele leer implementierte Methoden nich unbedingt das, was man effizient nennen kann. Die Codegröße steigt mit jeder unnötigen Funktion.
Da bei einer Schnittstellenimplementierung der Compiler die Implementierung aller abstrakten − rein virtuellen − Methoden verlangt, müssen Implementierungen angegeben werden. In Java-Bibliotheken wird diesem Problem oft mit einer Klasse begegnet, die die entsprechenden Methoden alle leer implementiert. Man gibt bei der Verwendung einer solchen Klasse allerdings die Überprüfung der vollständigen Implementierung durch den Compiler auf. Dafür müssen nur die Schnittstellenmethoden überschrieben werden, die man auch wirklich für den entsprechenden Fall benötigt. Wenn eine Operation definiert wird, die auf die Gesamtheit der Objekttypen angewendet werden soll, ist es weiterhin ratsam von der Schnittstelle abzuleiten. Die leer implementierende Klasse sollte nur für solche Operationen verwendet werden, die auf eine Untermenge der Objekttypen definiert ist. Auch in C++ lässt sich diese Technik anwenden. Allerdings verlangt die Anwendung dieser Technik, dass man die Hintergründe versteht und die genannten Vorüberlegungen bezüglich der Art der zu definierenden Operation anstellt. Auch der Leser des Codes muss mit diesen Überlegungen vertraut sein, um ihn interpretieren zu können. Die Verletzung des Kapselungsprinzips auf Klassenebene
Eine der Errungenschaften der Objektorientierung ist es, die Operationen auf die Daten dort zu definieren,
wo sich auch die zugehörigen Daten befinden − in den Klassen der Objekte. Dieses Systematisierungsprinzip
erleichtert den Überblick über komplexe Software dadurch, dass man eine Reduktion der zu
berücksichtigenden Einzelheiten erreicht. Man muss nur solche Attribute und Operationen berücksichtigen,
die zur gestellten Frage an den Quellcode geören. Die Klasse ist eine solche „Datenkapsel“.
Das in der Objektorientierung formulierte Delegationsprinzip geht davon aus, dass jedes Objekt
seine Attribute selbst verändert und auswertet. In der Schnittstelle der Klasse ist im Allgemeinen
kein Hinweis auf die interne Attributrepräsentation enthalten. Die Technik dieser systematischen
Abgrenzung wird durch das Sichtbarkeitskonzept der Programmiersprache geliefert. Die Schlüsselwörter
Der Visitor ist ein Muster, das ein Aufbrechen der Kapselung erzwingt. Das Visitorobjekt bearbeitet Attribute in einer dynamischen Objektstruktur. Die Operationen werden also von außen an die Objekte herangetragen. Dazu ist es notwendig, die Attribute die bearbeitet werden sollen entweder öffentlich zu machen oder mit einer speziellen für den Visitor vorbereiteten Schnittstelle zu exportieren. Die KostenEs stellt sich also die Frage nach den nachteiligen Auswirkungen dieser Kapselungsverletzung und damit nach den Kosten den das Visitorprinzip mit sich bringt.
Bedenkt man, dass das Besuchermuster eine Art Infrastruktur für eine beliebige Anzahl von Operationen
auf die Objektstruktur definiert, dann richten sich alle speziellen Operationen auch nach dieser Infrastruktur.
Diese Infrastruktur enthält als wesentlichen bestandteil eine Schnittstellenklasse Die Verletzung der Kapselung hat also immer dann schwer zu beherrschende Konsequenzen, wenn die Typen der Datenseite im Projekt nicht hinreichend definiert bzw. nicht stabil sind. Die Reduktion der Abhängigkeiten durch den azyklischen Besucher2)
Die von Alexandrescu in [AA01] beschriebene Variante des Visitors beruht im Wesentlichen darauf, dass
die Visitorschnittstelle in viele Einzelschnittstellen aufgespalten wird, die jeweils einem zu besuchenden
Typ der Datenseite assoziiert ist. Dieser Typ überprüft in seiner
Der Vorteil dieser Variante liegt darin, dass man bei neu hinzugefügten Typen der Datenseite keine Anpassung der bereits definierten Operationen durchführen muss, da deren Schnittstellen stabil bleiben. Die zyklische Abhängigkeit des Codes der Datenstruktur und der Visitorschnittstelle ist aufgehoben. Daher auch die Bezeichnung azyklischer Besucher. Man etabliert die Abhängigkeit erst zwischen den Operationen, die mit durch eine Besucherklasse realisiert werden sollen und den konkreten besuchten Typen.
Betrachten wir jedoch zunächst die Die Kosten des azyklischen BesuchersNatürlich hat auch der azyklische Besucher seine Kosten. Zum Beispiel verursacht er einen höheren Laufzeitaufwand durch den dynamischen Cast. Ein gewichtigeres Thema ist das folgende: Durch die Entkopplung der Besucherklasse von der zu besuchenden Datenklasse entfällt die Kontrolle durch den Compiler, ob überhaupt eine Operation für die letzere definiert worden ist. Der oben erwähnte Vorteil dieser Lösung ist also gleichzeitig auch ein Nachteil. Aber was ist schon perfekt in diesem Leben?
Stand: 10. April 2016
1) Im Folgetext einfach [GoF95] genannt - von „Gang of Four“. Literatur
AnhangDie gezeigten Beispiele wurden mit den folgenden Compilern getestet:
| ||||||||||||||||||||||||||||||||||||||||