Wzór gościa - Visitor pattern

W programowania obiektowego i oprogramowania inżynierii The gość wzorzec projektowy jest sposobem oddzielania algorytm z obiektu konstrukcji, na którym działa. Praktycznym rezultatem tej separacji jest możliwość dodawania nowych operacji do istniejących struktur obiektów bez modyfikowania struktur. Jest to jeden ze sposobów postępowania zgodnie z zasadą otwarte/zamknięte .

W istocie odwiedzający umożliwia dodawanie nowych funkcji wirtualnych do rodziny klas , bez modyfikowania klas. Zamiast tego tworzona jest klasa gościa, która implementuje wszystkie odpowiednie specjalizacje funkcji wirtualnej. Odwiedzający przyjmuje referencję do instancji jako dane wejściowe i realizuje cel poprzez podwójną wysyłkę .

Przegląd

Wzorzec projektowy Visitor jest jednym z dwudziestu trzech dobrze znanych wzorców projektowych GoF, które opisują, jak rozwiązywać powtarzające się problemy projektowe w celu projektowania elastycznego i wielokrotnego użytku oprogramowania zorientowanego obiektowo, czyli obiektów, które są łatwiejsze do wdrożenia, zmiany, testowania i ponowne użycie.

Jakie problemy może rozwiązać wzorzec projektowy Visitor?

  • Powinna istnieć możliwość zdefiniowania nowej operacji dla (niektórych) klas struktury obiektu bez zmiany klas.

Gdy często potrzebne są nowe operacje, a struktura obiektów składa się z wielu niepowiązanych klas, nie można dodawać nowych podklas za każdym razem, gdy wymagana jest nowa operacja, ponieważ „[..] rozłożenie wszystkich tych operacji na różne klasy węzłów prowadzi do powstania systemu, który jest trudny rozumieć, utrzymywać i zmieniać”.

Jakie rozwiązanie opisuje wzorzec projektowy Visitor?

  • Zdefiniuj osobny (odwiedzający) obiekt, który implementuje operację do wykonania na elementach struktury obiektu.
  • Klienci przechodzą przez strukturę obiektu i wywołują na elemencie operację wysyłającą accept (visitor) — która „wysyła” (deleguje) żądanie do „zaakceptowanego obiektu odwiedzającego”. Następnie obiekt odwiedzający wykonuje operację na elemencie („odwiedza element”).

Umożliwia to tworzenie nowych operacji niezależnie od klas struktury obiektu poprzez dodawanie nowych obiektów odwiedzających.

Zobacz także diagram klas i sekwencji UML poniżej.

Definicja

Gang of Four definiuje odwiedzającego jako:

Reprezentuje operację, która ma być wykonana na elementach struktury obiektu. Visitor pozwala zdefiniować nową operację bez zmiany klas elementów, na których operuje.

Charakter Visitor sprawia, że ​​jest to idealny wzorzec do podłączenia do publicznych interfejsów API, umożliwiając w ten sposób swoim klientom wykonywanie operacji na klasie przy użyciu klasy „odwiedzającej” bez konieczności modyfikowania źródła.

Zastosowania

Przeniesienie operacji do klas dla odwiedzających jest korzystne, gdy

  • wymaganych jest wiele niepowiązanych ze sobą operacji na strukturze obiektu,
  • klasy składające się na strukturę obiektu są znane i nie oczekuje się ich zmiany,
  • często trzeba dodawać nowe operacje,
  • algorytm obejmuje kilka klas struktury obiektu, ale pożądane jest zarządzanie nim w jednym miejscu,
  • algorytm musi działać w kilku niezależnych hierarchiach klas.

Wadą tego wzorca jest jednak to, że utrudnia on rozszerzanie hierarchii klas, ponieważ nowe klasy zazwyczaj wymagają visitdodania nowej metody do każdego odwiedzającego.

Przykład użycia

Rozważ projekt systemu komputerowego wspomagania projektowania 2D (CAD). W istocie istnieje kilka typów reprezentujących podstawowe kształty geometryczne, takie jak koła, linie i łuki. Elementy są uporządkowane w warstwy, a na szczycie hierarchii typów znajduje się rysunek, który jest po prostu listą warstw oraz kilkoma dodanymi właściwościami.

Podstawową operacją na tej hierarchii typów jest zapisanie rysunku w macierzystym formacie pliku systemu. Na pierwszy rzut oka dodanie lokalnych metod zapisu do wszystkich typów w hierarchii może wydawać się akceptowalne. Ale przydatna jest również możliwość zapisywania rysunków w innych formatach plików. Dodanie coraz większej liczby metod zapisywania w wielu różnych formatach plików szybko zaśmieca stosunkowo czystą oryginalną geometryczną strukturę danych.

Naiwnym sposobem rozwiązania tego byłoby zachowanie oddzielnych funkcji dla każdego formatu pliku. Taka funkcja zapisywania brałaby rysunek jako dane wejściowe, przemierzała go i kodowała do tego konkretnego formatu pliku. Ponieważ odbywa się to dla każdego dodanego innego formatu, kumuluje się duplikacja między funkcjami. Na przykład zapisanie kształtu okręgu w formacie rastrowym wymaga bardzo podobnego kodu, niezależnie od używanej formy rastrowej, i różni się od innych kształtów pierwotnych. Podobnie jest w przypadku innych prymitywnych kształtów, takich jak linie i wielokąty. W ten sposób kod staje się dużą zewnętrzną pętlą przechodzącą przez obiekty, z dużym drzewem decyzyjnym wewnątrz pętli pytającym o typ obiektu. Innym problemem związanym z tym podejściem jest to, że bardzo łatwo jest pominąć kształt w jednym lub kilku wygaszaczach lub wprowadzany jest nowy prymitywny kształt, ale procedura zapisywania jest implementowana tylko dla jednego typu pliku, a nie dla innych, co prowadzi do rozszerzenia kodu i konserwacji problemy.

Zamiast tego można zastosować wzorzec gościa. Koduje logiczną operację na całej hierarchii w jedną klasę zawierającą jedną metodę na typ. W przykładzie CAD każda funkcja zapisu byłaby zaimplementowana jako osobna podklasa Visitor. To usunęłoby wszelkie powielanie kontroli typu i kroków przechodzenia. Spowodowałoby to również, że kompilator narzekałby, gdyby jakiś kształt został pominięty.

Kolejnym motywem jest ponowne użycie kodu iteracyjnego. Na przykład iteracja po strukturze katalogów może być zaimplementowana za pomocą wzorca odwiedzających. Umożliwiłoby to tworzenie wyszukiwań plików, tworzenie kopii zapasowych plików, usuwanie katalogów itp. poprzez zaimplementowanie gościa dla każdej funkcji przy ponownym użyciu kodu iteracji.

Struktura

Diagram klas i sekwencji UML

Przykładowy diagram klas UML i diagram sekwencji dla wzorca projektowego Visitor.

Na powyższym diagramie klas UML klasaElementA nie implementuje bezpośrednio nowej operacji. Zamiast tego ElementAimplementuje operację wysyłania, accept(visitor) która „wysyła” (deleguje) żądanie do „zaakceptowanego obiektu odwiedzającego” ( visitor.visitElementA(this)). Te Visitor1narzędzia klasy operacji ( visitElementA(e:ElementA)).
ElementBnastępnie implementuje accept(visitor)wysyłając do visitor.visitElementB(this). Te Visitor1narzędzia klasy operacji ( visitElementB(e:ElementB)).

W UML sekwencji diagram przedstawia interakcje wykonywania czasu: ClientPrzedmiotem pochylnie elementy struktury (Object ElementA,ElementB) i połączenia accept(visitor)na każdym elemencie.
Po pierwsze, Clientwywołuje accept(visitor)on ElementA, który wywołuje visitElementA(this)zaakceptowany visitorobiekt. Sam element ( this) jest przekazywany do , visitoraby mógł „odwiedzić” ElementA(wywołać operationA()).
Następnie Clientrozmowy accept(visitor)na temat ElementB, który zwraca visitElementB(this)na visitor, że „wizyty” ElementB(połączenia operationB()).

Diagram klas

Odwiedzający w zunifikowanym języku modelowania (UML)
Gość w LePUS3 ( legenda )

Detale

Wzorzec odwiedzających wymaga języka programowania obsługującego pojedynczą wysyłkę , tak jak robią to popularne języki obiektowe (takie jak C++ , Java , Smalltalk , Objective-C , Swift , JavaScript , Python i C# ). W tym warunku rozważmy dwa obiekty, każdy z jakiegoś typu klasy; jeden jest nazywany żywiołem , a drugi gościem .

Użytkownik deklaruje visitmetodę, która przyjmuje element jako argument, dla każdej klasy elementów. Konkretni goście wywodzą się z klasy visitor i implementują te visitmetody, z których każda implementuje część algorytmu operującego na strukturze obiektu. Stan algorytmu jest utrzymywany lokalnie przez konkretną klasę odwiedzających.

Elementem deklaruje acceptmetodę przyjąć gościa, biorąc gościa jako argument. Elementy betonowe wywodzące się z klasy elementu implementują acceptmetodę. W najprostszej formie jest to tylko wywołanie metody odwiedzającego visit. Elementy złożone , które utrzymują listę obiektów podrzędnych, zazwyczaj iterują po nich, wywołując acceptmetodę każdego dziecka .

Klient tworzy strukturę obiektu, bezpośrednio lub pośrednio, a wystąpienie gości betonu. Gdy ma zostać wykonana operacja, która jest zaimplementowana przy użyciu wzorca Visitor, wywołuje acceptmetodę elementu(ów) najwyższego poziomu.

Gdy acceptmetoda jest wywoływana w programie, jej implementacja jest wybierana na podstawie zarówno dynamicznego typu elementu, jak i statycznego typu odwiedzającego. Gdy skojarzona visitmetoda jest wywoływana, jej implementacja jest wybierana na podstawie zarówno dynamicznego typu odwiedzającego, jak i statycznego typu elementu, znanego z implementacji acceptmetody, który jest taki sam jak dynamiczny typ elementu. (Jako bonus, jeśli odwiedzający nie może obsłużyć argumentu typu danego elementu, kompilator wykryje błąd.)

W związku z tym wdrożenie visitmetody jest wybierane na podstawie zarówno dynamicznego typu elementu, jak i dynamicznego typu odwiedzającego. To skutecznie realizuje podwójną wysyłkę . W przypadku języków, których systemy obiektowe obsługują wielokrotne wysyłanie, a nie tylko pojedyncze wysyłanie, takie jak Common Lisp lub C# za pośrednictwem Dynamic Language Runtime (DLR), implementacja wzorca odwiedzających jest znacznie uproszczona (inaczej Dynamic Visitor) poprzez umożliwienie użycia prostego przeciążania funkcji do obejmują wszystkie odwiedzane sprawy. Odwiedzający dynamiczny, o ile operuje tylko na danych publicznych, jest zgodny z zasadą open/closed (ponieważ nie modyfikuje istniejących struktur) oraz z zasadą pojedynczej odpowiedzialności (ponieważ implementuje wzorzec Visitor w osobnym komponencie).

W ten sposób można napisać jeden algorytm do przechodzenia przez wykres elementów, a podczas tego przechodzenia można wykonać wiele różnych operacji, dostarczając różne rodzaje odwiedzających do interakcji z elementami w oparciu o dynamiczne typy zarówno elementów, jak i goście.

Przykład C#

Ten przykład deklaruje oddzielną ExpressionPrintingVisitorklasę, która zajmuje się drukowaniem.

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);
		}
	}
}

Przykład smalltalka

W tym przypadku obiekt jest odpowiedzialny za to, aby wiedzieć, jak wydrukować się w strumieniu. Gość jest tutaj obiektem, a nie strumieniem.

"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.

Przykład C++

Źródła

#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);
  }
}

Wyjście

dispatching ArchivedFile
dispatching SplitFile
dispatching ExtractedFile

Idź na przykład

Go nie obsługuje przeciążania, więc metody odwiedzin wymagają różnych nazw.

Źródła

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)
}

Wyjście

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

Przykład Javy

Poniższy przykład jest w języku Java i pokazuje, jak można wydrukować zawartość drzewa węzłów (w tym przypadku opisującego komponenty samochodu). Zamiast tworzyć printmetody dla każdej podklasy węzła ( Wheel, Engine, Bodyi Car), jedna klasa użytkownika ( CarElementPrintVisitor) wykonuje wymagane działanie drukowania. Ponieważ różne podklasy węzłów wymagają nieco innych akcji do prawidłowego drukowania, CarElementPrintVisitorwysyła akcje na podstawie klasy argumentu przekazanego do jego visitmetody. CarElementDoVisitor, który jest analogiczny do operacji zapisywania dla innego formatu pliku, działa podobnie.

Diagram

Diagram UML przykładu wzorca Visitor z elementami samochodu

Źródła

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());
    }
}


Wyjście

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

Przykład wspólnego Lisp

Źródła

(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))

Wyjście

"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

Uwagi

other-objectParametr jest zbędny w traverse. Powodem jest to, że możliwe jest użycie funkcji anonimowej, która wywołuje pożądaną metodę docelową z obiektem przechwyconym leksykalnie:

(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)

Teraz wielokrotne wysyłanie występuje w wywołaniu wydanym z treści funkcji anonimowej, a więc traversejest to tylko funkcja mapująca, która rozdziela aplikację funkcji na elementy obiektu. W ten sposób znikają wszystkie ślady Wzorca Gościa, z wyjątkiem funkcji mapowania, w której nie ma dowodów na udział dwóch obiektów. Cała wiedza o istnieniu dwóch obiektów i wysyłaniu ich typów znajduje się w funkcji lambda.

Przykład w Pythonie

Python nie obsługuje przeciążania metod w klasycznym sensie (zachowanie polimorficzne w zależności od typu przekazanych parametrów), więc metody „odwiedzenia” dla różnych typów modeli muszą mieć różne nazwy.

Źródła

"""
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())

Wyjście

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.

Abstrakcja

Jeśli ktoś używa Pythona 3 lub nowszego, może wykonać ogólną implementację metody accept:

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

Można to rozszerzyć, aby iterować po kolejności rozwiązywania metod klasy, jeśli chcieliby skorzystać z już zaimplementowanych klas. Mogą również użyć funkcji zaczepienia podklasy, aby zdefiniować wyszukiwanie z wyprzedzeniem.

Powiązane wzorce projektowe

  • Wzorzec iteratora – definiuje zasadę przechodzenia, taką jak wzorzec gościa, bez rozróżniania typu w obrębie obiektów, przez które przechodzą
  • Kodowanie kościelne – pokrewne pojęcie z programowania funkcjonalnego, w którym oznakowane typy sum/suma mogą być modelowane za pomocą zachowań „odwiedzających” na takich typach, co umożliwia wzorcowi odwiedzającego emulację wariantów i wzorców .

Zobacz też

Bibliografia

Zewnętrzne linki