Besuchermuster - Visitor pattern

In der objektorientierten Programmierung und Software - Engineering , das Besucher - Entwurfsmuster ist eine Möglichkeit , eine der Trennung Algorithmus von einer Objektstruktur , auf die sie tätig ist . Ein praktisches Ergebnis dieser Trennung ist die Möglichkeit, bestehenden Objektstrukturen neue Operationen hinzuzufügen, ohne die Strukturen zu modifizieren. Es ist eine Möglichkeit, dem Offen/Geschlossen-Prinzip zu folgen .

Im Wesentlichen erlaubt es der Besucher neue Hinzufügen von virtuellen Funktionen zu einer Familie von Klassen , ohne die Klassen zu modifizieren. Stattdessen wird eine Besucherklasse erstellt, die alle entsprechenden Spezialisierungen der virtuellen Funktion implementiert. Der Besucher nimmt die Instanzreferenz als Eingabe und implementiert das Ziel durch Double Dispatch .

Überblick

Das Besucher-Designmuster ist eines der 23 bekannten GoF-Designmuster, die beschreiben, wie wiederkehrende Designprobleme gelöst werden, um flexible und wiederverwendbare objektorientierte Software zu entwerfen, d. h. Objekte, die einfacher zu implementieren, zu ändern, zu testen und Wiederverwendung.

Welche Probleme kann das Besucher-Entwurfsmuster lösen?

  • Es soll möglich sein, für (einige) Klassen einer Objektstruktur eine neue Operation zu definieren, ohne die Klassen zu ändern.

Wenn häufig neue Operationen benötigt werden und die Objektstruktur aus vielen unabhängigen Klassen besteht, ist es unflexibel, jedes Mal, wenn eine neue Operation erforderlich ist, neue Unterklassen hinzuzufügen, weil "[..] die Verteilung all dieser Operationen auf die verschiedenen Knotenklassen zu einem System führt, das schwer ist zu verstehen, zu pflegen und zu ändern."

Welche Lösung beschreibt das Besucher-Entwurfsmuster?

  • Definieren Sie ein separates (Besucher-)Objekt, das eine an Elementen einer Objektstruktur auszuführende Operation implementiert.
  • Clients durchqueren die Objektstruktur und rufen einen Dispatching-Vorgang akzeptieren (Besucher) für ein Element auf – das die Anforderung an das „akzeptierte Besucherobjekt“ „verteilt“ (delegiert). Das Besucherobjekt führt dann die Operation an dem Element aus ("besucht das Element").

Dadurch ist es möglich, durch Hinzufügen neuer Besucherobjekte unabhängig von den Klassen einer Objektstruktur neue Operationen zu erstellen.

Siehe auch das UML-Klassen- und Sequenzdiagramm unten.

Definition

Die Gang of Four definiert den Besucher als:

Stellt eine Operation dar, die an Elementen einer Objektstruktur ausgeführt werden soll. Mit Visitor können Sie eine neue Operation definieren, ohne die Klassen der Elemente zu ändern, für die sie ausgeführt wird.

Die Natur des Besuchers macht es zu einem idealen Muster, um sich in öffentliche APIs einzufügen, sodass seine Clients mithilfe einer "besuchenden" Klasse Operationen an einer Klasse ausführen können, ohne die Quelle ändern zu müssen.

Verwendet

Die Verlegung des Betriebs in Besucherklassen ist von Vorteil, wenn

  • viele unabhängige Operationen an einer Objektstruktur erforderlich sind,
  • die Klassen, aus denen die Objektstruktur besteht, bekannt sind und sich voraussichtlich nicht ändern werden,
  • neue Operationen müssen häufig hinzugefügt werden,
  • ein Algorithmus umfasst mehrere Klassen der Objektstruktur, aber es ist erwünscht, ihn an einem einzigen Ort zu verwalten,
  • ein Algorithmus muss über mehrere unabhängige Klassenhierarchien hinweg funktionieren.

Ein Nachteil dieses Musters besteht jedoch darin, dass es Erweiterungen der Klassenhierarchie erschwert, da neue Klassen normalerweise erfordern visit, dass jedem Besucher eine neue Methode hinzugefügt wird.

Anwendungsfallbeispiel

Betrachten Sie den Entwurf eines 2D - CAD-Systems ( Computer Aided Design ). Im Kern gibt es mehrere Typen, um grundlegende geometrische Formen wie Kreise, Linien und Bögen darzustellen. Die Elemente sind in Layer geordnet, und ganz oben in der Typhierarchie befindet sich die Zeichnung, bei der es sich einfach um eine Liste von Layern mit einigen hinzugefügten Eigenschaften handelt.

Ein grundlegender Vorgang bei dieser Typhierarchie ist das Speichern einer Zeichnung im systemeigenen Dateiformat. Auf den ersten Blick mag es akzeptabel erscheinen, allen Typen in der Hierarchie lokale Speichermethoden hinzuzufügen. Es ist aber auch nützlich, Zeichnungen in anderen Dateiformaten speichern zu können. Das Hinzufügen von immer mehr Methoden zum Speichern in viele verschiedene Dateiformate führt schnell zu einer Überfrachtung der relativ reinen ursprünglichen geometrischen Datenstruktur.

Ein naiver Weg, dies zu lösen, besteht darin, für jedes Dateiformat separate Funktionen bereitzustellen. Eine solche Speicherfunktion würde eine Zeichnung als Eingabe verwenden, sie durchlaufen und in dieses spezielle Dateiformat kodieren. Da dies für jedes hinzugefügte unterschiedliche Format erfolgt, häuft sich eine Duplizierung zwischen den Funktionen an. Beispielsweise erfordert das Speichern einer Kreisform in einem Rasterformat einen sehr ähnlichen Code, unabhängig davon, welche Rasterform verwendet wird, und unterscheidet sich von anderen primitiven Formen. Der Fall für andere primitive Formen wie Linien und Polygone ist ähnlich. Somit wird der Code zu einer großen äußeren Schleife, die die Objekte durchläuft, mit einem großen Entscheidungsbaum innerhalb der Schleife, der den Typ des Objekts abfragt. Ein weiteres Problem bei diesem Ansatz besteht darin, dass es sehr leicht ist, eine Form in einem oder mehreren Savern zu übersehen oder eine neue primitive Form eingeführt wird, aber die Speicherroutine wird nur für einen Dateityp und nicht für andere implementiert, was zu Codeerweiterung und -wartung führt Probleme.

Stattdessen kann das Besuchermuster angewendet werden. Es codiert eine logische Operation auf der gesamten Hierarchie in eine Klasse, die eine Methode pro Typ enthält. Im CAD-Beispiel würde jede Speicherfunktion als separate Visitor-Unterklasse implementiert. Dies würde alle doppelten Typprüfungen und Durchlaufschritte beseitigen. Außerdem würde sich der Compiler beschweren, wenn eine Form weggelassen wird.

Ein weiteres Motiv ist die Wiederverwendung von Iterationscode. Beispielsweise könnte das Iterieren über eine Verzeichnisstruktur mit einem Besuchermuster implementiert werden. Dies würde das Erstellen von Dateisuchen, Dateisicherungen, Entfernen von Verzeichnissen usw. ermöglichen, indem ein Besucher für jede Funktion implementiert wird, während der Iterationscode wiederverwendet wird.

Struktur

UML-Klassen- und Sequenzdiagramm

Ein Beispiel für ein UML-Klassendiagramm und ein Sequenzdiagramm für das Besucher-Entwurfsmuster.

Im obigen UML -KlassendiagrammElementA implementiert die Klasse keine neue Operation direkt. ElementAImplementiert stattdessen einen Dispatching-Vorgang accept(visitor) , der eine Anfrage an das "akzeptierte Besucherobjekt" ( visitor.visitElementA(this)) "verteilt" (delegiert ). Die Visitor1Klasse implementiert die Operation ( visitElementA(e:ElementA)).
ElementBdann implementiert accept(visitor)durch Versenden an visitor.visitElementB(this). Die Visitor1Klasse implementiert die Operation ( visitElementB(e:ElementB)).

Das UML- Sequenzdiagramm zeigt die Laufzeitinteraktionen: Das ClientObjekt durchläuft die Elemente einer Objektstruktur ( ElementA,ElementB) und ruft accept(visitor)jedes Element auf.
Zuerst die ClientAufrufe accept(visitor)von ElementA, die visitElementA(this)das akzeptierte visitorObjekt aufruft . Das Element selbst ( this) wird an das übergeben, visitordamit es "besuchen" ElementA(call operationA()) kann.
Danach Clientruft die accept(visitor)an ElementB, die die aufruft, visitElementB(this)die visitor"besucht" ElementB(anruft operationB()).

Klassen Diagramm

Besucher in Unified Modeling Language (UML)
Besucher in LePUS3 ( Legende )

Einzelheiten

Das Besuchermuster erfordert eine Programmiersprache , die Single Dispatch unterstützt , wie es gängige objektorientierte Sprachen (wie C++ , Java , Smalltalk , Objective-C , Swift , JavaScript , Python und C# ) tun. Betrachten Sie unter dieser Bedingung zwei Objekte, jedes von einem bestimmten Klassentyp; einer wird als Element bezeichnet , der andere als Besucher .

Der Besucher deklariert visitfür jede Elementklasse eine Methode, die das Element als Argument annimmt. Konkrete Besucher werden aus der Besucherklasse abgeleitet und implementieren diese visitMethoden, von denen jede einen Teil des Algorithmus implementiert, der auf der Objektstruktur arbeitet. Der Zustand des Algorithmus wird lokal von der konkreten Besucherklasse gepflegt.

Das Element deklariert eine acceptMethode zum Akzeptieren eines Besuchers, wobei der Besucher als Argument verwendet wird. Konkrete Elemente , abgeleitet von der Elementklasse, implementieren die acceptMethode. In seiner einfachsten Form ist dies nicht mehr als ein Aufruf der visitMethode des Besuchers . Zusammengesetzte Elemente, die eine Liste von untergeordneten Objekten verwalten, durchlaufen diese normalerweise und rufen die acceptMethode jedes untergeordneten Objekts auf .

Der Client erstellt direkt oder indirekt die Objektstruktur und instanziiert die konkreten Besucher. Wenn eine Operation ausgeführt werden soll, die unter Verwendung des Besuchermusters implementiert wird, ruft es die acceptMethode des/der Elemente der obersten Ebene auf.

Wenn die acceptMethode im Programm aufgerufen wird, wird ihre Implementierung basierend auf dem dynamischen Typ des Elements und dem statischen Typ des Besuchers ausgewählt. Wenn die zugehörige visitMethode aufgerufen wird, wird ihre Implementierung basierend auf dem dynamischen Typ des Besuchers und dem statischen Typ des Elements ausgewählt, wie aus der Implementierung der acceptMethode bekannt, der mit dem dynamischen Typ des Elements identisch ist. (Als Bonus fängt der Compiler den Fehler ab, wenn der Besucher ein Argument des Typs des angegebenen Elements nicht verarbeiten kann.)

Somit wird die Implementierung des visitVerfahrens basierend sowohl auf dem dynamischen Typ des Elements als auch auf dem dynamischen Typ des Besuchers ausgewählt. Dadurch wird der doppelte Versand effektiv implementiert . Für Sprachen, deren Objektsysteme mehrere Dispatchs unterstützen, nicht nur Single Dispatch, wie Common Lisp oder C# über die Dynamic Language Runtime (DLR), wird die Implementierung des Besuchermusters (auch bekannt als Dynamic Visitor) erheblich vereinfacht, indem die Verwendung einfacher Funktionsüberladungen ermöglicht wird alle besuchten Fälle abdecken. Ein dynamischer Besucher, sofern er nur mit öffentlichen Daten arbeitet, entspricht dem Open/Closed-Prinzip (da er bestehende Strukturen nicht verändert) und dem Single-Responsibility-Prinzip (da er das Besuchermuster in einer separaten Komponente implementiert).

Auf diese Weise kann ein Algorithmus geschrieben werden, um einen Graphen von Elementen zu durchqueren, und viele verschiedene Arten von Operationen können während dieser Durchquerung ausgeführt werden, indem verschiedene Arten von Besuchern bereitgestellt werden, um mit den Elementen zu interagieren, basierend auf den dynamischen Typen sowohl der Elemente als auch der Besucher.

C#-Beispiel

In diesem Beispiel wird eine separate ExpressionPrintingVisitorKlasse deklariert , die sich um das Drucken kümmert.

namespace Wikipedia
{
	public class ExpressionPrintingVisitor
	{
		public void PrintLiteral(Literal literal)
		{
			Console.WriteLine(literal.Value);
		}
		
		public void PrintAddition(Addition addition)
		{
			double leftValue = addition.Left.GetValue();
			double rightValue = addition.Right.GetValue();
			var sum = addition.GetValue();
			Console.WriteLine("{0} + {1} = {2}", leftValue, rightValue, sum);
		}
	}
	
	public abstract class Expression
	{	
		public abstract void Accept(ExpressionPrintingVisitor v);
		
		public abstract double GetValue();
	}

	public class Literal : Expression
	{
		public double Value { get; set; }

		public Literal(double value)
		{
			this.Value = value;
		}
		
		public override void Accept(ExpressionPrintingVisitor v)
		{
			v.PrintLiteral(this);
		}
		
		public override double GetValue()
		{
			return Value;
		}
	}

	public class Addition : Expression
	{
		public Expression Left { get; set; }
		public Expression Right { get; set; }

		public Addition(Expression left, Expression right)
		{
			Left = left;
			Right = right;
		}
		
		public override void Accept(ExpressionPrintingVisitor v)
		{
			Left.Accept(v);
			Right.Accept(v);
			v.PrintAddition(this);
		}
		
		public override double GetValue()
		{
			return Left.GetValue() + Right.GetValue();	
		}
	}

	public static class Program
	{
		public static void Main(string[] args)
		{
			// Emulate 1 + 2 + 3
			var e = new Addition(
				new Addition(
					new Literal(1),
					new Literal(2)
				),
				new Literal(3)
			);
			
			var printingVisitor = new ExpressionPrintingVisitor();
			e.Accept(printingVisitor);
		}
	}
}

Smalltalk-Beispiel

In diesem Fall liegt es in der Verantwortung des Objekts zu wissen, wie es sich selbst in einem Stream drucken kann. Der Besucher hier ist dann das Objekt, nicht der Strom.

"There's no syntax for creating a class. Classes are created by sending messages to other classes."
WriteStream subclass: #ExpressionPrinter
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'Wikipedia'.

ExpressionPrinter>>write: anObject
    "Delegates the action to the object. The object doesn't need to be of any special
    class; it only needs to be able to understand the message #putOn:"
    anObject putOn: self.
    ^ anObject.

Object subclass: #Expression
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'Wikipedia'.

Expression subclass: #Literal
    instanceVariableNames: 'value'
    classVariableNames: ''
    package: 'Wikipedia'.

Literal class>>with: aValue
    "Class method for building an instance of the Literal class"
    ^ self new
        value: aValue;
        yourself.

Literal>>value: aValue
  "Setter for value"
  value := aValue.

Literal>>putOn: aStream
    "A Literal object knows how to print itself"
    aStream nextPutAll: value asString.

Expression subclass: #Addition
    instanceVariableNames: 'left right'
    classVariableNames: ''
    package: 'Wikipedia'.

Addition class>>left: a right: b
    "Class method for building an instance of the Addition class"
    ^ self new
        left: a;
        right: b;
        yourself.

Addition>>left: anExpression
    "Setter for left"
    left := anExpression.

Addition>>right: anExpression
    "Setter for right"
    right := anExpression.

Addition>>putOn: aStream
    "An Addition object knows how to print itself"
    aStream nextPut: $(.
    left putOn: aStream.
    aStream nextPut: $+.
    right putOn: aStream.
    aStream nextPut: $).

Object subclass: #Program
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'Wikipedia'.

Program>>main
    | expression stream |
    expression := Addition
                    left: (Addition
                            left: (Literal with: 1)
                            right: (Literal with: 2))
                    right: (Literal with: 3).
    stream := ExpressionPrinter on: (String new: 100).
    stream write: expression.
    Transcript show: stream contents.
    Transcript flush.

C++-Beispiel

Quellen

#include <iostream>
#include <vector>

class AbstractDispatcher;  // Forward declare AbstractDispatcher

class File {  // Parent class for the elements (ArchivedFile, SplitFile and
              // ExtractedFile)
 public:
  // This function accepts an object of any class derived from
  // AbstractDispatcher and must be implemented in all derived classes
  virtual void Accept(AbstractDispatcher& dispatcher) = 0;
};

// Forward declare specific elements (files) to be dispatched
class ArchivedFile;
class SplitFile;
class ExtractedFile;

class AbstractDispatcher {  // Declares the interface for the dispatcher
 public:
  // Declare overloads for each kind of a file to dispatch
  virtual void Dispatch(ArchivedFile& file) = 0;
  virtual void Dispatch(SplitFile& file) = 0;
  virtual void Dispatch(ExtractedFile& file) = 0;
};

class ArchivedFile : public File {  // Specific element class #1
 public:
  // Resolved at runtime, it calls the dispatcher's overloaded function,
  // corresponding to ArchivedFile.
  void Accept(AbstractDispatcher& dispatcher) override {
    dispatcher.Dispatch(*this);
  }
};

class SplitFile : public File {  // Specific element class #2
 public:
  // Resolved at runtime, it calls the dispatcher's overloaded function,
  // corresponding to SplitFile.
  void Accept(AbstractDispatcher& dispatcher) override {
    dispatcher.Dispatch(*this);
  }
};

class ExtractedFile : public File {  // Specific element class #3
 public:
  // Resolved at runtime, it calls the dispatcher's overloaded function,
  // corresponding to ExtractedFile.
  void Accept(AbstractDispatcher& dispatcher) override {
    dispatcher.Dispatch(*this);
  }
};

class Dispatcher : public AbstractDispatcher {  // Implements dispatching of all
                                                // kind of elements (files)
 public:
  void Dispatch(ArchivedFile&) override {
    std::cout << "dispatching ArchivedFile" << std::endl;
  }

  void Dispatch(SplitFile&) override {
    std::cout << "dispatching SplitFile" << std::endl;
  }

  void Dispatch(ExtractedFile&) override {
    std::cout << "dispatching ExtractedFile" << std::endl;
  }
};

int main() {
  ArchivedFile archived_file;
  SplitFile split_file;
  ExtractedFile extracted_file;

  std::vector<File*> files = {
      &archived_file,
      &split_file,
      &extracted_file,
  };

  Dispatcher dispatcher;
  for (File* file : files) {
    file->Accept(dispatcher);
  }
}

Ausgabe

dispatching ArchivedFile
dispatching SplitFile
dispatching ExtractedFile

Beispiel gehen

Go unterstützt keine Überladung, daher benötigen die Besuchsmethoden unterschiedliche Namen.

Quellen

package main

import "fmt"

type Visitor interface {
	visitWheel(wheel Wheel) string
	visitEngine(engine Engine) string
	visitBody(body Body) string
	visitCar(car Car) string
}

type element interface {
	Accept(visitor Visitor) string
}

type Wheel struct {
	name string
}

func (w *Wheel) Accept(visitor Visitor) string {
	return visitor.visitWheel(*w)
}

func (w *Wheel) getName() string {
	return w.name
}

type Engine struct{}

func (e *Engine) Accept(visitor Visitor) string {
	return visitor.visitEngine(*e)
}

type Body struct{}

func (b *Body) Accept(visitor Visitor) string {
	return visitor.visitBody(*b)
}

type Car struct {
	engine Engine
	body   Body
	wheels [4]Wheel
}

func (c *Car) Accept(visitor Visitor) string {
	elements := []element{
		&c.engine,
		&c.body,
		&c.wheels[0],
		&c.wheels[1],
		&c.wheels[2],
		&c.wheels[3],
	}
	res := visitor.visitCar(*c)
	for _, elem := range elements {
		res += elem.Accept(visitor)
	}
	return res
}

type PrintVisitor struct{}

func (pv *PrintVisitor) visitWheel(wheel Wheel) string {
	return fmt.Sprintln("visiting", wheel.getName(), "wheel")
}
func (pv *PrintVisitor) visitEngine(engine Engine) string {
	return fmt.Sprintln("visiting engine")
}
func (pv *PrintVisitor) visitBody(body Body) string {
	return fmt.Sprintln("visiting body")
}
func (pv *PrintVisitor) visitCar(car Car) string {
	return fmt.Sprintln("visiting car")
}

/* output:
visiting car
visiting engine
visiting body
visiting front left wheel
visiting front right wheel
visiting back left wheel
visiting back right wheel
*/
func main() {
	car := Car{
		engine: Engine{},
		body:   Body{},
		wheels: [4]Wheel{
			{"front left"},
			{"front right"},
			{"back left"},
			{"back right"},
		},
	}

	visitor := PrintVisitor{}
	res := car.Accept(&visitor)
	fmt.Println(res)
}

Ausgabe

visiting car
visiting engine
visiting body
visiting front left wheel
visiting front right wheel
visiting back left wheel
visiting back right wheel

Java-Beispiel

Das folgende Beispiel ist in der Sprache Java und zeigt, wie der Inhalt eines Knotenbaums (in diesem Fall die Komponenten eines Autos beschreibend) gedruckt werden kann. Anstatt printMethoden für jede Knotenunterklasse ( Wheel, Engine, Body, und Car) zu erstellen , führt eine Besucherklasse ( CarElementPrintVisitor) die erforderliche Druckaktion durch. Da verschiedene Knoten-Unterklassen leicht unterschiedliche Aktionen erfordern, um richtig zu drucken, verteilt CarElementPrintVisitorAktionen basierend auf der Klasse des an ihre visitMethode übergebenen Arguments . CarElementDoVisitor, das einem Speichervorgang für ein anderes Dateiformat entspricht, tut dies ebenfalls.

Diagramm

UML-Diagramm des Besuchermuster-Beispiels mit Car Elements

Quellen

import java.util.List;

interface CarElement {
    void accept(CarElementVisitor visitor);
}

interface CarElementVisitor {
    void visit(Body body);
    void visit(Car car);
    void visit(Engine engine);
    void visit(Wheel wheel);
}

class Wheel implements CarElement {
  private final String name;

  public Wheel(final String name) {
      this.name = name;
  }

  public String getName() {
      return name;
  }

  @Override
  public void accept(CarElementVisitor visitor) {
      /*
       * accept(CarElementVisitor) in Wheel implements
       * accept(CarElementVisitor) in CarElement, so the call
       * to accept is bound at run time. This can be considered
       * the *first* dispatch. However, the decision to call
       * visit(Wheel) (as opposed to visit(Engine) etc.) can be
       * made during compile time since 'this' is known at compile
       * time to be a Wheel. Moreover, each implementation of
       * CarElementVisitor implements the visit(Wheel), which is
       * another decision that is made at run time. This can be
       * considered the *second* dispatch.
       */
      visitor.visit(this);
  }
}

class Body implements CarElement {
  @Override
  public void accept(CarElementVisitor visitor) {
      visitor.visit(this);
  }
}

class Engine implements CarElement {
  @Override
  public void accept(CarElementVisitor visitor) {
      visitor.visit(this);
  }
}

class Car implements CarElement {
    private final List<CarElement> elements;

    public Car() {
        this.elements = List.of(
            new Wheel("front left"), new Wheel("front right"),
            new Wheel("back left"), new Wheel("back right"),
            new Body(), new Engine()
        );
    }

    @Override
    public void accept(CarElementVisitor visitor) {
        for (CarElement element : elements) {
            element.accept(visitor);
        }
        visitor.visit(this);
    }
}

class CarElementDoVisitor implements CarElementVisitor {
    @Override
    public void visit(Body body) {
        System.out.println("Moving my body");
    }

    @Override
    public void visit(Car car) {
        System.out.println("Starting my car");
    }

    @Override
    public void visit(Wheel wheel) {
        System.out.println("Kicking my " + wheel.getName() + " wheel");
    }

    @Override
    public void visit(Engine engine) {
        System.out.println("Starting my engine");
    }
}

class CarElementPrintVisitor implements CarElementVisitor {
    @Override
    public void visit(Body body) {
        System.out.println("Visiting body");
    }

    @Override
    public void visit(Car car) {
        System.out.println("Visiting car");
    }

    @Override
    public void visit(Engine engine) {
        System.out.println("Visiting engine");
    }

    @Override
    public void visit(Wheel wheel) {
        System.out.println("Visiting " + wheel.getName() + " wheel");
    }
}

public class VisitorDemo {
    public static void main(final String[] args) {
        Car car = new Car();

        car.accept(new CarElementPrintVisitor());
        car.accept(new CarElementDoVisitor());
    }
}


Ausgabe

Visiting front left wheel
Visiting front right wheel
Visiting back left wheel
Visiting back right wheel
Visiting body
Visiting engine
Visiting car
Kicking my front left wheel
Kicking my front right wheel
Kicking my back left wheel
Kicking my back right wheel
Moving my body
Starting my engine
Starting my car

Allgemeines Lisp-Beispiel

Quellen

(defclass auto ()
  ((elements :initarg :elements)))

(defclass auto-part ()
  ((name :initarg :name :initform "<unnamed-car-part>")))

(defmethod print-object ((p auto-part) stream)
  (print-object (slot-value p 'name) stream))

(defclass wheel (auto-part) ())

(defclass body (auto-part) ())

(defclass engine (auto-part) ())

(defgeneric traverse (function object other-object))

(defmethod traverse (function (a auto) other-object)
  (with-slots (elements) a
    (dolist (e elements)
      (funcall function e other-object))))

;; do-something visitations

;; catch all
(defmethod do-something (object other-object)
  (format t "don't know how ~s and ~s should interact~%" object other-object))

;; visitation involving wheel and integer
(defmethod do-something ((object wheel) (other-object integer))
  (format t "kicking wheel ~s ~s times~%" object other-object))

;; visitation involving wheel and symbol
(defmethod do-something ((object wheel) (other-object symbol))
  (format t "kicking wheel ~s symbolically using symbol ~s~%" object other-object))

(defmethod do-something ((object engine) (other-object integer))
  (format t "starting engine ~s ~s times~%" object other-object))

(defmethod do-something ((object engine) (other-object symbol))
  (format t "starting engine ~s symbolically using symbol ~s~%" object other-object))

(let ((a (make-instance 'auto
                        :elements `(,(make-instance 'wheel :name "front-left-wheel")
                                    ,(make-instance 'wheel :name "front-right-wheel")
                                    ,(make-instance 'wheel :name "rear-left-wheel")
                                    ,(make-instance 'wheel :name "rear-right-wheel")
                                    ,(make-instance 'body :name "body")
                                    ,(make-instance 'engine :name "engine")))))
  ;; traverse to print elements
  ;; stream *standard-output* plays the role of other-object here
  (traverse #'print a *standard-output*)

  (terpri) ;; print newline

  ;; traverse with arbitrary context from other object
  (traverse #'do-something a 42)

  ;; traverse with arbitrary context from other object
  (traverse #'do-something a 'abc))

Ausgabe

"front-left-wheel"
"front-right-wheel"
"rear-left-wheel"
"rear-right-wheel"
"body"
"engine"
kicking wheel "front-left-wheel" 42 times
kicking wheel "front-right-wheel" 42 times
kicking wheel "rear-left-wheel" 42 times
kicking wheel "rear-right-wheel" 42 times
don't know how "body" and 42 should interact
starting engine "engine" 42 times
kicking wheel "front-left-wheel" symbolically using symbol ABC
kicking wheel "front-right-wheel" symbolically using symbol ABC
kicking wheel "rear-left-wheel" symbolically using symbol ABC
kicking wheel "rear-right-wheel" symbolically using symbol ABC
don't know how "body" and ABC should interact
starting engine "engine" symbolically using symbol ABC

Anmerkungen

Der other-objectParameter ist in überflüssig traverse. Der Grund ist, dass es möglich ist, eine anonyme Funktion zu verwenden, die die gewünschte Zielmethode mit einem lexikalisch erfassten Objekt aufruft:

(defmethod traverse (function (a auto)) ;; other-object removed
  (with-slots (elements) a
    (dolist (e elements)
      (funcall function e)))) ;; from here too

  ;; ...

  ;; alternative way to print-traverse
  (traverse (lambda (o) (print o *standard-output*)) a)

  ;; alternative way to do-something with
  ;; elements of a and integer 42
  (traverse (lambda (o) (do-something o 42)) a)

Nun erfolgt der mehrfache Dispatch im Aufruf, der vom Rumpf der anonymen Funktion ausgegeben wird, und traverseist somit nur eine Zuordnungsfunktion, die eine Funktionsanwendung auf die Elemente eines Objekts verteilt. Damit verschwinden alle Spuren des Besuchermusters, mit Ausnahme der Mapping-Funktion, bei der es keine Hinweise auf die Beteiligung zweier Objekte gibt. Das gesamte Wissen darüber, dass es zwei Objekte und einen Dispatch für ihre Typen gibt, ist in der Lambda-Funktion enthalten.

Python-Beispiel

Python unterstützt kein Methodenüberladen im klassischen Sinne (polymorphes Verhalten je nach Typ der übergebenen Parameter), daher müssen die "visit"-Methoden für die verschiedenen Modelltypen unterschiedliche Namen haben.

Quellen

"""
Visitor pattern example.
"""

from abc import ABCMeta, abstractmethod

NOT_IMPLEMENTED = "You should implement this."

class CarElement:
    __metaclass__ = ABCMeta
    @abstractmethod
    def accept(self, visitor):
        raise NotImplementedError(NOT_IMPLEMENTED)

class Body(CarElement):
    def accept(self, visitor):
        visitor.visitBody(self)

class Engine(CarElement):
    def accept(self, visitor):
        visitor.visitEngine(self)

class Wheel(CarElement):
    def __init__(self, name):
        self.name = name
    def accept(self, visitor):
        visitor.visitWheel(self)

class Car(CarElement):
    def __init__(self):
        self.elements = [
            Wheel("front left"), Wheel("front right"),
            Wheel("back left"), Wheel("back right"),
            Body(), Engine()
        ]

    def accept(self, visitor):
        for element in self.elements:
            element.accept(visitor)
        visitor.visitCar(self)

class CarElementVisitor:
    __metaclass__ = ABCMeta
    @abstractmethod
    def visitBody(self, element):
        raise NotImplementedError(NOT_IMPLEMENTED)
    @abstractmethod
    def visitEngine(self, element):
        raise NotImplementedError(NOT_IMPLEMENTED)
    @abstractmethod
    def visitWheel(self, element):
        raise NotImplementedError(NOT_IMPLEMENTED)
    @abstractmethod
    def visitCar(self, element):
        raise NotImplementedError(NOT_IMPLEMENTED)

class CarElementDoVisitor(CarElementVisitor):
    def visitBody(self, body):
        print("Moving my body.")
    def visitCar(self, car):
        print("Starting my car.")
    def visitWheel(self, wheel):
        print("Kicking my {} wheel.".format(wheel.name))
    def visitEngine(self, engine):
        print("Starting my engine.")

class CarElementPrintVisitor(CarElementVisitor):
    def visitBody(self, body):
        print("Visiting body.")
    def visitCar(self, car):
        print("Visiting car.")
    def visitWheel(self, wheel):
        print("Visiting {} wheel.".format(wheel.name))
    def visitEngine(self, engine):
        print("Visiting engine.")

car = Car()
car.accept(CarElementPrintVisitor())
car.accept(CarElementDoVisitor())

Ausgabe

Visiting front left wheel.
Visiting front right wheel.
Visiting back left wheel.
Visiting back right wheel.
Visiting body.
Visiting engine.
Visiting car.
Kicking my front left wheel.
Kicking my front right wheel.
Kicking my back left wheel.
Kicking my back right wheel.
Moving my body.
Starting my engine.
Starting my car.

Abstraktion

Wenn man Python 3 oder höher verwendet, kann man eine allgemeine Implementierung der Accept-Methode vornehmen:

class Visitable:
    def accept(self, visitor):
        lookup = "visit_" + type(self).__qualname__.replace(".", "_")
        return getattr(visitor, lookup)(self)

Man könnte dies erweitern, um über die Methodenauflösungsreihenfolge der Klasse zu iterieren, wenn man auf bereits implementierte Klassen zurückgreifen möchte. Sie könnten auch die Hook-Funktion der Unterklasse verwenden, um die Suche im Voraus zu definieren.

Verwandte Designmuster

  • Iteratormuster – definiert ein Traversierungsprinzip wie das Besuchermuster, ohne eine Typunterscheidung innerhalb der durchlaufenen Objekte vorzunehmen
  • Kirche Codierung - ein verwandtes Konzept aus der funktionalen Programmierung, bei der Vereinigung / sum Typen markiert kann modelliert werden , indem das Verhalten der „Besucher“ auf solche Typen, und was es ermöglicht , das Besuchermuster zu emulieren Varianten und Mustern .

Siehe auch

Verweise

Externe Links