Visitor Pattern

Aus THM-Wiki
Wechseln zu: Navigation, Suche

Vorwort

Liebe Leser zu diesem Artikel können Sie das Lernmodul Visitor Pattern herunterladen. Das Lernmdul enthält den selben (und noch mehr) Inhalt wie dieser Wiki-Artikel jedoch werden die Inhalte mithilfe von Lernvideos vermittelt.

Media:VisitorPattern_Lernmodul.zip

Einleitung

Das Entwurfsmuster Visitor gehört zu der Gruppe der Verhaltensmuster. Verhaltensmuster befassen sich mit Algorithmen und der Zuweisung von Zuständigkeiten zu Objekten. Solche Muster beschreiben nicht nur Muster von Objekten und Klassen sondern auch die Muster der Interaktion zwischen ihnen. Verhaltensmuster beschreiben komplexe Kontrollflüsse, die zur Laufzeit schwer nachvollziebar sind. Wobei hier nicht der Kontrollfluss sondern die Art und Weiße wie Objekte mit einander interagieren von Bedeutung ist.

Mit dem Visitor Pattern ist es möglich verwandte Vorgänge auf eine Gruppe von Klassen auszuführen und diese Opertionen in einer einzigen Klasse zu plazieren. Das Ziel des Pattern ist die Code-Wartbarkeit. In solchen Situationen erschwert das halten der Operationen in den jeweiligen Klassen die Code-Wartbarkeit. Das Visitor Pattern stellt hier ein übergeordnetes Framework zu Verfügung welche die Erweiterung und die Wartung des Codes erleichtert.


Einsatz des Visitor Pattern

Das Visitor Entwurfsmuster wird eingesetzt wenn folgende drei Bedingungen erfüllt sind:

  • Wenn ein System eine Reihe verwandter Klassen enthällt.
  • Wenn nicht triviale Operationen auf eine Reihe oder auf alle Verwandten Klassen ausgeführt werden müssen.
  • Wenn gemeinsame Operationen durchgeführt werden sollen, jedoch diese für die jeweiligen Klassen im Detail anders implementiert werden müssen.

UML Notation

VisitorPatternUML.png

Beschreibung

Das Muster fordert dass alle Klassen auf den dieses Muster Vorgänge durchführt, diese werden auch Elemente genannt, in irgend einer Form über die Methode "accept" verfügen. Die Accept-Methode eines Elementes wird immer dann aufgerufen wenn der Visitor eine Operation auf dieses Element anwenden will. Das Argument der Accept-Methode ist eine Instanz eines Visitors. In der Implementierung der Accept-Methode wird die visit-Methode des betretenden Visitors für die eigene Instance aufgerufen. Die Visitor Implementierung enthält für jedes Teilelement eine visit-Methode welches das Element spezifischen Operationen schließlich ausführt.

Vor- und Nachteile

Vorteile

  • Aufgrund der Struktur des Visitor Patterns lässt sich ein System sehr einfach mit neuem Verhalten erweitern. Das Visitor Pattern dient als Framework für weitere Visitor. Durch erstellen einer Klasse, welche das Interface „Visitor“ implementiert kann ein bestehendes System an zukünftige Anforderungen leicht angepasst werden.
  • Das Visitor Pattern ermöglicht die Zentralisierung von funktionellem Code. Durch Verwendung des Musters werden unterschiedliche Operationen auf unterschiedliche Objekte durchgeführt, die Implementierung der Operation befindet sich zentral in der Visitor-Klasse und muss nicht in den einzelnen Klassen ergänzt werden.
  • Das Visitor Objekt wird verwendet um die Elemente einer Struktur zu besuchen, dadurch lassen sich Daten die gesammelt werden müssen oder auch Zwischenergebnisse von Operationen zentral sammeln, speichern und weiterverarbeiten.


Nachteile

  • Geringe Veränderungen der Elementklasse lösen mit einer hohen Wahrscheinlichkeit die Notwendigkeit aus die Visitor-Klasse zu überarbeiten.
  • Bei jeder nachträglichen Einführung eines neuen Elementes muss das Visitor-Interface mit der entsprechenden visit-Methode für das neue Element erweitern werden. Bereits bestehende Konkrete Visitor, welches das Visitor-Interface implementieren, müssen mit der visit-Methode für das neue Element nachträglich erweitert werden.
  • Das Muster verletzt das Prinzip der Codekapselung der Objektorientierten Programmierung. Das Visitor-Muster beinhaltet Code welcher auf eine Reihe unterschiedlicher Objekte angewendet wird, welche nicht in der eigenen Klasse enthalten sind. Ein Element der Struktur muss den Konkreten Visitor nicht kennen. Der Visitor muss jedoch die Elemente kennen, da er mit den Methoden der Elementklasse interagiert.

Varianten des Entwurfsmusters

Wie bei vielen anderen Mustern können die Interfaces „Visitor“ und „Element“ auch als abstrakte Klassen vorgegeben werden. Die Traversierung der Struktur kann auch unterschiedliche Arten umgesetzt werden. Die Traversierung der Struktur kann direkt in der „KonkretVisitor“ Klasse erfolgen. Es ist auch möglich die Struktur mit einer externen Klasse wie dem Iterator zu Traversieren. Um diese Funktionalität um zusetzen wird das Visitor Pattern mit dem Iterator-Pattern kombiniert.

Verwandte Muster

Folgende Muster sind mit dem Visitor Patern verwandt. Sie gehören zu einer Reihe von Mustern die die in Verbindung mit Visitor Pattern verwendet werden um eine Struktur zu durchlaufen.

  • Interpreter: Der Visitor wird hier eingesetzt um die Interpretation einer Operation zu zentralisieren.
  • Iterator: Verwendung zum durchlaufen einer Struktur.
  • Composite: Wird in Verbindung mit dem Visitor Verwendet um Baumstrukturen zu Durchlaufen.

Implementierungsbeispiel in Java

Beschreibung

Stellen Sie sich vor Sie sind ein Projektmanager und möchten Informationen wie Kosten oder die benötigte Bearbeitungszeit ihres Projektes abschätzen. Das Projektmanagement System welches Sie einsetzen besteht aus den folgenden Klassen.

  • Projekt: Die Wurzel der Struktur, repräsentiert das Projekt selbst.
  • Task: Arbeitsschritt innerhalb des Projektes
  • DependentTask: Ein Arbeitsschritt welcher von einem anderen abhängig ist.
  • Deliverable: Ein Bericht, Resultat über ein Bereits abgeschlossenen Teilschritt.
  • Interface ProjectItem: dieses Interface implementieren alle Elemente eines Projektes.


UML-Diagramm des Projektes

Die UML-Diagramme für das oben genannte Beispiel wurde mit hilfe des Enterprise Architect erstellt.

ProjectUMLDiagramm.png

Der Enterprise Architect ist ein umfangreiches Modellierungswerkzeug. Das Tool erstellt Quelltext und Dokumentation aus UML-Notationen in 13 Programmiersprachen. Es bietet unter anderem Integration in Visual Studio und Eclipse, Testvorgaben und Reportgenerierung. Das Werkzeug verwendet UML 2.1.

Weitere Informationen und auch eine Testversion dieses Werkzeuges erhalten Sie aus der Sparx System Homepage.

Aus den UML-Diagrammen wurden mit Hilfe des EA-Tools der Quellcode für die benötigten Klassen generiert. Anschließend wurde der erzeugte Quellecode mithilfe von Eclipse editiert.


Reverse Engineering

Einsatz des Visitor Patterns beim auswerten von Parsebäumen

Das hier durchgeführte Reverse Engineering bezieht sich auf eine Implementierung eines Parsers in JavaCC. Parser werden eingesetzt um Ausdrücke auf die Korrektheit ihrer Syntax zu Überprüfen. Der Parser ist in der Lage einen Regulären Ausdruck zu Parsen. Dabei erstellt der Parser einen sogenannten Abstrakten Syntaxbaum. Die Knoten des Baumes sind mit einander Verwandt. Hier wird das Visitor Pattern eingesetzt um die Baumstruktur zu Traversieren. Dabei wird überprüft ob die Interpretation eines Models zu einer wahren aussage führt. Bei der Traversierung der Struktur wird ein Bitvektor mit der Interpretation bis hin zu den Blättern weitergereicht. Die Blätter werden anschließend mit den Werten des Bitvektors belegt und ausgewertet. Die Knoten des Baumes sind Rechenvorschriften die Festlegen wie die Belegung ausgewertet wird. Das Durchlaufen der Struktur erfolgt rekursiv.

Beispiel: (a|b) & (c -> (!d))

Wird dieser Ausdruck mit einem Parser zerlegt sol entsteht folgender Sysntaxbaum:

Parsebaum.png


Die Runden Elemente AND, OR, IMPLIES und Not sind Knoten des Baumes. Die Rechtecke Stehen für die Belegung der atomaren ausdrücke a, b, c und d. Mit dem Einsatz des Visitor Patterns wollen wir überprüfen ob die oben aufgeführte Belegung zu einer wahren Aussage führt.

Falls Sie erfahren möchten wie man mit JavaCC und JJTree Parser für unerschiedliche Anwendungen Parser Implemetiert dann schauen sie sich auf der JavaCC-Homepage um. Hier erfahren sie alles über Parserimplementierung mit Hilfe der Tools JavaCC un JJTree.

Ein JavaCC Eclipse Plugin und eine Anleitung wie man es mit Eclipse verwendet finden Sie hier.

UML-Diagramm

Durch Reverse Engineering der hier beschriebenen Anwendung erhalten wir folgendes UML-Klassendiagramm

UML.png


Variante des Musters

Man erkennt aus dem UML-Diagramm das es sich hier um eine Variante des Entwurfsmusters handelt. Zeichen dafür sind:

  • Das Interfase „Node“ beinhaltet zwar Methoden die grundsätzlich für eine „Node“ Objekt gelten jedoch wird die Methode „accept“ hier nicht deklariert.
  • Die Abstrakte Klasse „SimpleNode“ wird vom Interface „Node“ abgeleitet, hier werden die Methoden des Interfaces Implementiert. Zusätzlich enthält diese Klasse die Methoden „jjtAccept“ und „childrenAccept“.
  • Die Elemente des Syntaxbaumes werden anschließen von der „SimpleNode“ Klasse abgeleitet und enthalten die Spezifische Implementierung der „jjtAccept“ Methode.
  • Zusätzlicher Argument „Objekt data“ bei der Methode „jjtAccept“

Die Elemente

Die Klassen auf der linken Seite des Diagrammes, also all die mit AST beginnen, stellen die Knoten des Syntaxbaumes dar. Abhängig von dem eingegebenen Ausdruck wird beim Parsen ein abstrakter Syntaxbaum aufgebaut welcher aus den hier aufgeführten Elementen besteht. Die Reihenfolge der Knoten ist abhängig vom eingegebenen Ausdruck. Jedes dieser Elemente enthält die Implementierung der Methode „jjtAccept“, sie wird wie folgt Implementiert:

public Object jjtAccept(MyParserVisitor visitor, Object data) {
   return visitor.visit(this, data);
 }

Sie ermöglicht den Aufruf der Methode „visit“ für das entsprechende Element. Zusätzlich können hier noch weitere Daten an die „visit“ Methode übergeben.

Die Klasse „SimpleNode“ und das Interface „Node“

Durch das Einführen der abstrakten Klasse SimpleNode wird die Implementierung in den Elementen übersichtlich gehalten. Die Klasse SimpleNode implementiert alle Methoden die das Interface Node vorschreibt. Somit wird die Implementierung der Methoden in den Elementen überflüssig. Ein weiterer Vorteil ist dass hier die Methode childrenAccept exestiert. Hier wird die Traversierung der Baumstruktur Implementiert. Diese Methode kann optional zu der jjtAccept Mehode vewendet werden.

public Object childrenAccept(MyParserVisitor visitor, Object data) {
   if (children != null) {
     for (int i = 0; i < children.length; ++i) {
       ((SimpleNode) children[i]).jjtAccept(visitor, data);
     }
   }
   return data;
 }


So eine Art der Implementierung der Accept Methode zum durchlaufen von Strukturen kann natürlich verwendet werden ist in dieser Anwendung jedoch nicht zu gebrauchen. Wir wollen nämlich die Struktur nicht einfach nur durchlaufen sondern auch bestimmte Operationen mit den Objekten durchführen welche die obere Implementierung nicht zulässt.

Es gilt grundsätzlich für den Einsatz des Visitor Patterns:

Als erstes muss überprüft werden ob es eine optimale Variante des Musters für einen Specifischen Sachverhalt gibt. Wenn es eine gibt dann sollte dieser auch verwendet werden. Wenn es keine ideale Variante gibt gilt: Zur Navigation auf der Objektstruktur sollte der Code in die Visitor Klasse verlagert werden und nicht in die Elemente.


Das Interface Visitor und dessen Implementierung

Das Visitor Interface enthält für jedes Element der Struktur eine „visit“ Methode. Die Klasse „MyVisitor“ implementiert das Interface „Visitor“ und die Spezifische Funktionalität der Jeweiligen „visit“ Methode. Die Navigation auf der Objektstruktur wird ebenfalls hier implementiert diese erfolgt rekursiv. Implementierung:

package ModellChecking;
import java.util.*;
public class MyVisitor implements MyParserVisitor {

        public Object visit(SimpleNode node, Object data) {
		// TODO Auto-generated method stub
		return null;
	}

	public Object visit(ASTStart node, Object data) {
		// TODO Auto-generated method stub
		SimpleNode c1 = (SimpleNode)node.jjtGetChild(0);
		return (Boolean)c1.jjtAccept(this,data);
	}

	public Object visit(ASTatomic node, Object data) {
		// TODO Auto-generated method stub
		Hashtable belegung = (Hashtable)data;
                return new Boolean((Boolean)belegung.get(node.getAtomicExpr()));
	}

	public Object visit(ASTNOT node, Object data) {
		// TODO Auto-generated method stub
 		 SimpleNode c1 = (SimpleNode)node.jjtGetChild(0);
		 Boolean r1 = (Boolean)c1.jjtAccept(this,data);
		 if (true == r1.booleanValue())
		    return new Boolean(false);
		return new Boolean(true);
	}
	public Object visit(ASTAND node, Object data) {
		// TODO Auto-generated method stub
		SimpleNode c1 = (SimpleNode)node.jjtGetChild(0);
		SimpleNode c2 = (SimpleNode)node.jjtGetChild(1);
		Boolean r1 = (Boolean)c1.jjtAccept(this,data);
		Boolean r2 = (Boolean)c2.jjtAccept(this,data);
		if (r1.booleanValue() && r2.booleanValue())
		    return new Boolean(true);
		return new Boolean(false);
	}
	public Object visit(ASTOR node, Object data) {
		// TODO Auto-generated method stub
		SimpleNode c1 = (SimpleNode)node.jjtGetChild(0);
		SimpleNode c2 = (SimpleNode)node.jjtGetChild(1);
		Boolean r1 = (Boolean)c1.jjtAccept(this,data);
		Boolean r2 = (Boolean)c2.jjtAccept(this,data);
		if (r1.booleanValue() || r2.booleanValue())
		    return new Boolean(true);
		return new Boolean(false);
	}
	public Object visit(ASTIMPLIES node, Object data) {
		SimpleNode c1 = (SimpleNode)node.jjtGetChild(0);
		SimpleNode c2 = (SimpleNode)node.jjtGetChild(1);
		Boolean r1 = (Boolean)c1.jjtAccept(this,data);
		Boolean r2 = (Boolean)c2.jjtAccept(this,data);
		if (!(r2.booleanValue()))
			{
			 if (r1.booleanValue())
		         return new Boolean(false);
			}
			 // TODO Auto-generated method stub
        return new Boolean(true);
	}
	public Object visit(ASTPHI node, Object data) {
		// TODO Auto-generated method stub
		return null;
	}
	public Object visit(ASTconstant node, Object data) {
		// TODO Auto-generated method stub
		return new Boolean(false);
	}
}