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ą visit
dodania 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
Na powyższym diagramie klas UML klasaElementA
nie implementuje bezpośrednio nowej operacji. Zamiast tego ElementA
implementuje operację wysyłania, accept(visitor)
która „wysyła” (deleguje) żądanie do „zaakceptowanego obiektu odwiedzającego” ( visitor.visitElementA(this)
). Te Visitor1
narzędzia klasy operacji ( visitElementA(e:ElementA)
).
ElementB
następnie implementuje accept(visitor)
wysyłając do visitor.visitElementB(this)
. Te Visitor1
narzędzia klasy operacji ( visitElementB(e:ElementB)
).
W UML sekwencji diagram
przedstawia interakcje wykonywania czasu: Client
Przedmiotem pochylnie elementy struktury (Object ElementA,ElementB
) i połączenia accept(visitor)
na każdym elemencie.
Po pierwsze, Client
wywołuje accept(visitor)
on
ElementA
, który wywołuje visitElementA(this)
zaakceptowany visitor
obiekt. Sam element ( this
) jest przekazywany do , visitor
aby mógł „odwiedzić” ElementA
(wywołać operationA()
).
Następnie Client
rozmowy accept(visitor)
na temat
ElementB
, który zwraca visitElementB(this)
na visitor
, że „wizyty” ElementB
(połączenia operationB()
).
Diagram klas
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 visit
metodę, która przyjmuje element jako argument, dla każdej klasy elementów. Konkretni goście wywodzą się z klasy visitor i implementują te visit
metody, 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 accept
metodę przyjąć gościa, biorąc gościa jako argument. Elementy betonowe wywodzące się z klasy elementu implementują accept
metodę. 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 accept
metodę 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 accept
metodę elementu(ów) najwyższego poziomu.
Gdy accept
metoda jest wywoływana w programie, jej implementacja jest wybierana na podstawie zarówno dynamicznego typu elementu, jak i statycznego typu odwiedzającego. Gdy skojarzona visit
metoda jest wywoływana, jej implementacja jest wybierana na podstawie zarówno dynamicznego typu odwiedzającego, jak i statycznego typu elementu, znanego z implementacji accept
metody, 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 visit
metody 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ą ExpressionPrintingVisitor
klasę, 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ć print
metody dla każdej podklasy węzła ( Wheel
, Engine
, Body
i 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, CarElementPrintVisitor
wysyła akcje na podstawie klasy argumentu przekazanego do jego visit
metody. CarElementDoVisitor
, który jest analogiczny do operacji zapisywania dla innego formatu pliku, działa podobnie.
Diagram
Ź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-object
Parametr 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 traverse
jest 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
- The Visitor Family of Design Patterns at the Wayback Machine (archiwum 22 października 2015). Dodatkowe archiwa: 12 kwietnia 2004 , 5 marca 2002 . Wstępny rozdział z The Principles, Patterns and Practices of Agile Software Development , Robert C. Martin , Prentice Hall
- Wzorzec odwiedzających w UML i LePUS3 (język opisu projektu)
- Artykuł „ Komponentyzacja: przykład gościa autorstwa Bertranda Meyera i Karine Arnout, Computer (IEEE), vol. 39, nr 7, lipiec 2006, strony 23-30.
- Artykuł A Teoretyczna rekonstrukcja wzorca odwiedzających
- Artykuł „ Istota wzorca gości ” autorstwa Jensa Palsberga i C. Barry'ego Jaya . 1997 IEEE-CS COMPSAC artykuł pokazujący, że metody accept() są niepotrzebne, gdy dostępne jest odbicie; wprowadza termin „Walkabout” dla techniki.
- Artykuł „ Czas na refleksję ” Bruce'a Wallace'a – podtytuł „Możliwości refleksji w Javie 1.2 eliminują uciążliwe metody accept() z wzorca odwiedzającego”
- Visitor Pattern wykorzystujący odbicie (java).
- PerfectJPattern Open Source Project , zapewnia bezkontekstową i bezpieczną typowo implementację wzorca odwiedzającego w Javie opartą na delegatach.
- Wzór projektu gościa