Visitor Pattern: Was steckt hinter dem Besucher-Entwurfsmuster?
Die objektorientierte Programmierung (OOP), die als Unterform des imperativen Programmierparadigmas eingestuft wird, hat in den vergangenen Jahren stark an Bedeutung gewonnen. Die Option, alle Bausteine eines Software-Projekts als Objekt zu beschreiben, dessen Verhalten sich durch die entsprechenden Klassen bestimmen lässt, bietet einige entscheidende Vorzüge gegenüber anderen Programmierstilen. Insbesondere die Möglichkeit, entworfene Programmteile problemlos wiederzuverwenden, ist für viele Entwickler ein klares Argument für die Verwendung der OOP.
Um diese Wiederverwendbarkeit sowie die Implementierung, Anpassbarkeit und Testfähigkeit der eingebauten Objekte noch einfacher zu gestalten, wurden in dem Buch „Design Patterns: Elements of Reusable Object-Oriented Software“ die GoF-Entwurfsmuster vorgestellt. Eines dieser über 20 verschiedenen Design Patterns ist das sogenannte Visitor Pattern bzw. Visitor Design Pattern (zu Deutsch: Besucher-Entwurfsmuster), das in den nachfolgenden Abschnitten ausführlich vorgestellt werden soll.
Was ist das Visitor Pattern?
Das Visitor Design Pattern, kurz Visitor Pattern, stellt eine Musterlösung dafür dar, einen Algorithmus von der Objektstruktur zu trennen, auf der er arbeitet. Es beschreibt einen Weg, um neue Operationen zu bestehenden Objektstrukturen hinzufügen, ohne dass diese Strukturen hierfür modifiziert werden müssen. Dank dieser Eigenschaft stellt das Visitor Pattern eine mögliche Option für die Umsetzung des Open-Closed-Prinzips (OCP) dar. Dieses Prinzip der objektorientierten Software-Entwicklung beruht darauf, dass jegliche Software-Einheiten – wie Module, Klassen oder Methoden – gleichzeitig offen (open) für Erweiterungen und verschlossen (closed) für Modifikationen sind.
Im Deutschen bezeichnet man das Visitor Pattern auch als Besucher-Entwurfsmuster bzw. Besucher-Muster.
Das Visitor Pattern ist eines von insgesamt 23 Entwurfsmuster (Kategorie: Verhaltensmuster), die 1994 von den Informatikern Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides beschrieben und veröffentlicht wurden. Da die Vier in der Entwicklerszene auch als „Gang of Four“ (dt. Viererbande) – kurz GoF – bekannt sind, hat sich für die besagten Patterns der Name GoF-Entwurfsmuster eingebürgert.
Welchen Zweck erfüllt das Visitor Design Pattern?
Besteht die Objektstruktur aus vielen unverbundenen Klassen und es werden häufig neue Operationen benötigt, ist es für Entwickler höchst unkomfortabel, für jede neue Operation eine neue Unterklasse zu implementieren. Das Resultat dieser Vorgehensweise: Ein System mit diversen verschiedenen Knotenklassen, das nicht nur schwer zu verstehen, sondern auch schwer zu pflegen und zu modifizieren ist. Die entscheidende Instanz des Visitor Patterns, der Besucher (Visitor), erlaubt daher das Hinzufügen neuer, virtueller Funktionen zu einer Familie von Klassen, ohne diese Klassen modifizieren zu müssen.
Virtuelle Funktionen bzw. Methoden definieren auszuführende Zielfunktionen, deren Ziel zur Kompilierungszeit noch nicht bekannt sein muss. Sie stellen ein wichtiges Werkzeug objektorientierter Sprachen dar.
Das Besucher-Entwurfsmuster sieht vor, dass ein solches Visitor-Objekt separat definiert wird – mit dem Plan, eine Operation zu implementieren, die auf einem Element bzw. mehreren Elementen der Objektstruktur ausgeführt wird. Clients, die auf die Objektstruktur zugreifen, rufen dann auf dem betroffenen Element Methoden wie „accept(visitor)“ auf, die die Anfrage an das akzeptierte Visitor-Objekt delegieren. Infolgedessen kann das Besucher-Objekt die jeweilige Operation ausführen.
Grafische Darstellung des Visitor Patterns (UML-Diagramm)
Das Zusammenspiel zwischen den vorhandenen Elementen und den eingebundenen Visitor-Objekten nach dem Visitor Design Pattern lässt sich am besten anhand einer grafischen Darstellung der Beziehungen und Vorgänge einer möglichen objektorientierten Software verdeutlichen. Optimal zu diesem Zweck geeignet ist die beliebte Modellierungssprache UML (Unified Modeling Language), die aus diesem Grund auch im nachfolgenden Klassendiagramm für das Visitor Pattern verwendet wurde.
Die Vor- und Nachteile des Visitor Patterns
Das Visitor Pattern stellt einen bereits ausgefeilten, gut funktionierenden Weg dar, um bestehende Einheiten einer objektorientierten Software zu erweitern. Soll eine neue Operation hinzugefügt werden, gelingt dies problemlos durch die Definition eines neuen Besuchers. Nebenbei ermöglicht diese Vorgehensweise auch, jeglichen funktionellen Code zu zentralisieren: Die jeweilige Implementierung einer Operation befindet sich zentral in der Visitor-Klasse und muss nicht extra in den einzelnen anderen Klassen ergänzt werden. Der entscheidende Vorteil einer Software nach Visitor Design Pattern besteht zusammengefasst darin, dass der zugrundeliegende Quellcode der verwendeten Objekte nicht ständig anzupassen ist. Die Logik wird stattdessen auf die als Stellvertreter agierenden Besucher und Besucher-Klassen aufgeteilt.
Natürlich ist aber auch das Besucher-Entwurfsmuster nicht in allen Punkten perfekt. Wer nach den Grundsätzen dieses Musters arbeitet, muss sich folgender Sache bewusst sein: Bereits geringe Veränderungen an der Klasse eines Elements haben in den meisten Fällen zur Folge, dass auch zugewiesene Visitor-Klassen anzupassen sind. Zudem bleibt zusätzliche Arbeit bei der nachträglichen Einführung neuer Elemente nicht erspart, denn auch für diese gilt es, visit()-Methoden zu implementieren, die wiederum in den ConcreteVisitor-Klassen zu ergänzen sind. Die hervorragende Erweiterbarkeit der Software-Einheiten ist also an ein gewisses Maß an Aufwand geknüpft.
Wo kommt das Visitor Pattern zum Einsatz?
Das Visitor Design Pattern kann wiederkehrende Aufgabenstellungen in der Software-Entwicklung erheblich vereinfachen. Insbesondere für Entwickler, die dem objektorientierten Programmierparadigma folgen, lohnt sich eine Auseinandersetzung mit dem Entwurfsmuster. Seit seiner Vorstellung im Jahr 1994 hat sich das Muster folglich in der Programmierszene zu einer festen Größe entwickelt, wobei der Typ des Software-Projekts im Prinzip keine entscheidende Rolle für den Nutzfaktor des Patterns spielt. Auch hinsichtlich der profitierenden Programmiersprachen bestehen grundsätzlich keine konkreten Einschränkungen für das Musterprinzip, mit Ausnahme der Tatsache, dass es insbesondere auf das objektorientierte Paradigma zugeschnitten ist.
Beliebte Sprachen, in denen das Visitor Pattern eine elementare Rolle spielt, sind unter anderem folgende:
- C++
- C#
- Java
- PHP
- Python
- JavaScript
- Golang
Praxisnahes Beispiel für den Einsatz des Visitor Patterns
Den Nutzen und Zweck des Visitor Patterns zu verstehen, ist von außen betrachtet gar nicht so einfach. Wer heute programmieren lernt, kommt allerdings automatisch mit dem Muster und dessen Umsetzung in Berührung.
Als leichter greifbare Analogie aus dem realen Leben wird für das Besucher-Entwurfsmuster häufig die Fahrt mit einem Taxi aufgeführt: Ein Kunde bestellt ein Taxi, das auf Wunsch bis an seine Haustür kommt. Sitzt die Person erst einmal in dem „besuchenden“ Taxi, ist dieses (bzw. der Fahrer) gänzlich in Kontrolle über den Transport der Person.
Auch das Einkaufen in einem Supermarkt wird häufig als Bild für die Funktionsweise des Visitor Patterns verwendet: Die einkaufende Person sammelt im Einkaufswagen die gewünschte Ware, die bildlich für das Set an Elementen der Objektstruktur steht. An der Kasse angekommen fungiert das kassierende Personal als Visitor, der die Preise und das Gewicht der einzelnen Shopping-Güter (bzw. Elemente) scannt, um die anfallenden Gesamtkosten zu errechnen.
Beispiel-Code nach Visitor-Pattern-Ansatz (PHP)
Abschließend präsentiert der nachfolgende Code eine einfache, grundlegende Umsetzung des Visitor Patterns in PHP.
return 'B';
}
public function getData() {
return $this->the_data;
}
public function entgegennehmen(Besucher $besucher) {
$besucher->BesuchVonElementB($this);
}
}
abstract class Besucher {
abstract function BesuchVonElementA(ElementA $elem);
abstract function BesuchVonElementB(ElementB $elem);
}
class Besucher1 extends Besucher {
private $characteristics;
public function getCharacs() {
return $this->characteristics;
}
public function BesuchVonElementA(ElementA $elem) {
$this->characteristics = 'Info:'.$elem->getInfo();
}
public function BesuchVonElementB(ElementB $elem) {
$this->characteristics = 'DATA:'.$elem->getData().'!!';
}
}
function Test() {
write_line('Testanfang');
// Objektstruktur
$elemente = array (
new ElementA('Hallo', 'Neu!!'),
new ElementB('Endlich.'),
);
$bes1 = new Besucher1();
foreach ($elemente as $element) {
$element->entgegennehmen($bes1);
write_line('Nach Besuch von Element '.$element->getName().': '.$bes1- >getCharacs());
}
}
function write_line($text) {
print $text.'<br/>';
}
Test();
Die Ausgabe dieses Beispiel-Code-Snippets sieht dann wie folgt aus:
Testanfang
Nach Besuch von Element A: Info:[Hallo--Neu!!]
Nach Besuch von Element B: DATA:(Endlich.)!!