Kiedy powinienem użyć Wzorca Projektowego Odwiedzającego? [Zamknięte]


315

W blogach wciąż widuję odniesienia do schematu odwiedzających, ale muszę przyznać, że po prostu tego nie rozumiem. Przeczytałem artykuł w Wikipedii dotyczący tego wzoru i rozumiem jego mechanikę, ale nadal nie jestem pewien, kiedy go użyję.

Jako ktoś, kto niedawno naprawdę wziął wzór dekoratora i teraz widzi go do użycia absolutnie wszędzie, chciałbym móc naprawdę intuicyjnie zrozumieć ten pozornie przydatny wzór.


7
W końcu dostałem go po przeczytaniu tego artykułu przez Jermeya Millera na mojej jeżynie, kiedy utknąłem czekając w holu przez dwie godziny. Jest długi, ale daje wspaniałe wyjaśnienie podwójnej wysyłki, odwiedzającego i złożonego oraz tego, co możesz z nimi zrobić.
George Mauer,


3
Wzór gościa? Który? Chodzi o to: istnieje wiele nieporozumień i czystego zamieszania wokół tego wzorca projektowego. Napisałem i artykuł, który, jak mam nadzieję, porządkuje ten chaos: rgomes-info.blogspot.co.uk/2013/01/…
Richard Gomes

Jeśli chcesz mieć obiekty funkcyjne na typach danych unii, potrzebujesz wzorca odwiedzającego. Możesz się zastanawiać, jakie są obiekty funkcji i typy danych unii, to warto przeczytać ccs.neu.edu/home/matthias/htdc.html
Wei Qiu

Przykłady tutaj i tutaj .
jaco0646

Odpowiedzi:


315

Nie jestem zbyt zaznajomiony ze schematem Visitor. Zobaczmy, czy dobrze to zrozumiałem. Załóżmy, że masz hierarchię zwierząt

class Animal {  };
class Dog: public Animal {  };
class Cat: public Animal {  };

(Załóżmy, że jest to złożona hierarchia z dobrze ustalonym interfejsem).

Teraz chcemy dodać do hierarchii nową operację, a mianowicie chcemy, aby każde zwierzę wydało dźwięk. O ile hierarchia jest tak prosta, możesz to zrobić z prostym polimorfizmem:

class Animal
{ public: virtual void makeSound() = 0; };

class Dog : public Animal
{ public: void makeSound(); };

void Dog::makeSound()
{ std::cout << "woof!\n"; }

class Cat : public Animal
{ public: void makeSound(); };

void Cat::makeSound()
{ std::cout << "meow!\n"; }

Ale postępując w ten sposób, za każdym razem, gdy chcesz dodać operację, musisz zmodyfikować interfejs dla każdej pojedynczej klasy hierarchii. Teraz załóżmy, że jesteś zadowolony z oryginalnego interfejsu i chcesz wprowadzić jak najmniej modyfikacji.

Wzorzec gościa pozwala przenieść każdą nową operację do odpowiedniej klasy, a interfejs hierarchii należy rozszerzyć tylko raz. Zróbmy to. Najpierw definiujemy operację abstrakcyjną (klasę „Visitor” w GoF ), która ma metodę dla każdej klasy w hierarchii:

class Operation
{
public:
    virtual void hereIsADog(Dog *d) = 0;
    virtual void hereIsACat(Cat *c) = 0;
};

Następnie modyfikujemy hierarchię, aby akceptować nowe operacje:

class Animal
{ public: virtual void letsDo(Operation *v) = 0; };

class Dog : public Animal
{ public: void letsDo(Operation *v); };

void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }

class Cat : public Animal
{ public: void letsDo(Operation *v); };

void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }

Wreszcie, wdrażamy rzeczywistą operację, nie zmieniając ani kota, ani psa :

class Sound : public Operation
{
public:
    void hereIsADog(Dog *d);
    void hereIsACat(Cat *c);
};

void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!\n"; }

void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!\n"; }

Teraz możesz dodawać operacje bez modyfikowania hierarchii. Oto jak to działa:

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}

19
S.Lott, chodzenie po drzewie nie jest właściwie schematem odwiedzin. (Jest to „hierarchiczny wzorzec gościa”, który jest myląco zupełnie inny.) Nie ma sposobu, aby pokazać wzorzec GoF gościa bez użycia dziedziczenia lub implementacji interfejsu.
hojny

14
@Knownasilya - To nieprawda. & -Operator podaje adres obiektu dźwiękowego, który jest potrzebny interfejsowi. letsDo(Operation *v) potrzebuje wskaźnika.
AquilaRapax

3
tylko dla jasności, czy ten przykład wzorca projektowania gości jest prawidłowy?
godzilla

4
Po długim zastanowieniu zastanawiam się, dlaczego nazywasz dwie metody tutaj Isadog i hereIacACat, mimo że już przekazujesz Psa i Kota metodom. Wolałbym prosty performTask (Object * obj), a ty rzucasz ten obiekt w klasę Operation. (oraz w języku obsługującym nadpisywanie, nie ma potrzeby przesyłania)
Abdalrahman Shatou

6
W swoim „głównym” przykładzie na końcu: theSound.hereIsACat(c)czy wykonałbyś tę pracę, jak uzasadniasz wszystkie koszty ogólne wprowadzone przez wzorzec? podwójna wysyłka jest uzasadnieniem.
franssu

131

Przyczyną twojego pomieszania jest prawdopodobnie to, że Gość jest fatalnym błędem. Wielu (wybitnych 1 !) Programistów potknęło się o ten problem. W rzeczywistości implementuje podwójne wysyłanie w językach, które nie obsługują go natywnie (większość z nich nie obsługuje).


1) Moim ulubionym przykładem jest Scott Meyers, uznany autor „Effective C ++”, który nazwał to jedno ze swoich najważniejszych C ++ aha! chwile kiedykolwiek .


3
+1 „nie ma wzorca” - idealna odpowiedź. najbardziej pozytywna odpowiedź dowodzi, że wielu programistów c ++ jeszcze nie zdaje sobie sprawy z ograniczeń funkcji wirtualnych związanych z polimorfizmem „adhoc” za pomocą wyliczenia typu i przypadku przełączania (sposób c). Korzystanie z wirtualnego może być fajniejsze i niewidoczne, ale nadal ogranicza się do pojedynczej wysyłki. Moim osobistym zdaniem jest to największa wada c ++.
user3125280,

@ user3125280 Przeczytałem już 4/5 artykułów i rozdział Wzorce projektowe na temat wzorca gościa i żaden z nich nie wyjaśnia przewagi używania tego niejasnego wzoru nad wzorem skrzynki lub kiedy możesz użyć jednego nad drugim. Dzięki za przynajmniej poruszenie tego tematu!
spinkus

4
@sam Jestem prawie pewien, że to wyjaśniają - to ta sama przewaga , którą zawsze uzyskuje się z polimorfizmu podklas / środowiska wykonawczego w porównaniu z switch: switchtwardym kodowaniem decyzji po stronie klienta (duplikacja kodu) i nie oferuje statycznego sprawdzania typu ( sprawdź kompletność i odrębność spraw itp.). Wzorzec gościa jest weryfikowany przez moduł sprawdzania typu i zwykle upraszcza kod klienta.
Konrad Rudolph

@KonradRudolph dzięki za to. Zauważmy jednak, że nie jest to wyraźnie omówione na przykład w Patterns lub w Wikipedii. Nie zgadzam się z tobą, ale możesz argumentować, że korzystanie ze case stmt również przynosi korzyści, więc to dziwne, że ogólnie nie ma kontrastu: 1. nie potrzebujesz metody accept () na obiektach swojej kolekcji. 2. Gość ~ może obsługiwać obiekty nieznanego typu. Zatem sprawa stmt wydaje się lepiej dopasowana do działania na strukturach obiektowych z wymienną kolekcją typów. Wzory przyznają, że wzorzec gościa nie jest dobrze dopasowany do takiego scenariusza (s. 333).
spinkus

1
@SamPinkus konrad na miejscu - dlatego virtualpodobne funkcje są tak przydatne w nowoczesnych językach programowania - są podstawowym składnikiem rozszerzalnych programów - moim zdaniem sposób c (zagnieżdżony przełącznik lub dopasowanie wzorców itp. W zależności od wybranego języka) jest znacznie bardziej przejrzysty w kodzie, który nie musi być rozszerzalny, i byłem mile zaskoczony widząc ten styl w skomplikowanym oprogramowaniu, takim jak prover 9. Co ważniejsze, każdy język, który chce zapewnić rozszerzalność, powinien prawdopodobnie uwzględniać lepsze wzorce wysyłania niż rekurencyjne pojedyncze wysyłanie (tj. gość).
user3125280

84

Wszyscy tutaj mają rację, ale myślę, że nie odnosi się do „kiedy”. Po pierwsze, z wzorców projektowych:

Użytkownik umożliwia zdefiniowanie nowej operacji bez zmiany klas elementów, na których ona działa.

Pomyślmy teraz o prostej hierarchii klas. Mam klasy 1, 2, 3 i 4 oraz metody A, B, C i D. Układaj je jak w arkuszu kalkulacyjnym: klasy to linie, a metody to kolumny.

Obecnie projekt zorientowany obiektowo zakłada, że ​​istnieje większe prawdopodobieństwo, że wyhodujesz nowe klasy niż nowe metody, więc dodanie większej liczby linii, że tak powiem, jest łatwiejsze. Po prostu dodajesz nową klasę, określasz różnice w tej klasie i dziedziczy resztę.

Czasami jednak klasy są względnie statyczne, ale trzeba często dodawać więcej metod - dodając kolumny. Standardowym sposobem w projekcie OO byłoby dodanie takich metod do wszystkich klas, co może być kosztowne. Wzorzec Visitor ułatwia to.

Nawiasem mówiąc, jest to problem, który zamierza rozwiązać wzór Scali.


Dlaczego miałbym używać wzorca odwiedzających tylko klasę użyteczności. mogę wywołać moją klasę narzędziową w następujący sposób: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Co za różnica ? oboje robią oddzielne obawy, prawda? mam nadzieję, że możesz pomóc.
j2emanue

@ j2emanue Ponieważ wzorzec użytkownika używa prawidłowego przeciążenia użytkownika w czasie wykonywania. Podczas gdy Twój kod wymaga rzutowania typu, aby wywołać prawidłowe przeciążenie.
Odmowa dostępu

czy jest z tym wzrost wydajności? Chyba unika tego dobry pomysł
j2emanue

@ j2emanue chodzi o napisanie kodu zgodnego z zasadą open / closed, a nie ze względu na wydajność. Zobacz otwarte zamknięte wuja Boba butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Odmowa dostępu

22

Visitor wzornictwo działa bardzo dobrze dla „cyklicznych” struktur, takich jak drzewa katalogów, struktury XML lub konturów dokumentów.

Obiekt Visitor odwiedza każdy węzeł w strukturze rekurencyjnej: każdy katalog, każdy znacznik XML, cokolwiek. Obiekt Visitor nie przechodzi przez strukturę. Zamiast tego metody Visitor są stosowane do każdego węzła struktury.

Oto typowa struktura węzła rekurencyjnego. Może to być katalog lub znacznik XML. [Jeśli jesteś osobą Java, wyobraź sobie wiele dodatkowych metod budowania i utrzymywania listy dzieci.]

class TreeNode( object ):
    def __init__( self, name, *children ):
        self.name= name
        self.children= children
    def visit( self, someVisitor ):
        someVisitor.arrivedAt( self )
        someVisitor.down()
        for c in self.children:
            c.visit( someVisitor )
        someVisitor.up()

visitMetoda dotyczy obiektu odwiedzających do każdego węzła w strukturze. W tym przypadku jest to gość odgórny. Możesz zmienić strukturę visitmetody, aby wykonać oddolne lub inne uporządkowanie.

Oto superklasa dla odwiedzających. Jest używany przez visitmetodę. „Dociera” do każdego węzła w strukturze. Ponieważ visitmetoda wywołuje upi down, użytkownik może śledzić głębokość.

class Visitor( object ):
    def __init__( self ):
        self.depth= 0
    def down( self ):
        self.depth += 1
    def up( self ):
        self.depth -= 1
    def arrivedAt( self, aTreeNode ):
        print self.depth, aTreeNode.name

Podklasa może wykonywać takie czynności, jak zliczanie węzłów na każdym poziomie i gromadzenie listy węzłów, generując niezłą hierarchiczną liczbę sekcji ścieżek.

Oto aplikacja. To buduje strukturę drzewa, someTree. Tworzy Visitor, dumpNodes.

Następnie stosuje się dumpNodesdo drzewa. dumpNodeObiekt będzie „wizyta” Każdy węzeł w drzewie.

someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )

visitAlgorytm TreeNode zapewni, że każdy TreeNode zostanie użyty jako argument w arrivedAtmetodzie gościa .


8
Jak stwierdzili inni, jest to „hierarchiczny wzorzec odwiedzających”.
PPC-Coder

1
@ PPC-Coder Jaka jest różnica między „hierarchicznym wzorcem użytkowników” a wzorcem użytkowników?
Tim Lovell-Smith,

3
Hierarchiczny wzorzec gości jest bardziej elastyczny niż klasyczny wzorzec gości. Na przykład za pomocą wzoru hierarchicznego można śledzić głębokość przejścia i decydować, która gałąź ma przejść lub zatrzymać wszystkie ruchy razem. Klasyczny gość nie ma tej koncepcji i odwiedzi wszystkie węzły.
PPC-Coder

18

Jednym ze sposobów, aby na to spojrzeć, jest to, że wzorzec odwiedzających pozwala klientom dodawać dodatkowe metody do wszystkich klas w określonej hierarchii klas.

Jest to przydatne, gdy masz dość stabilną hierarchię klas, ale zmieniają się wymagania dotyczące tego, co należy zrobić z tą hierarchią.

Klasyczny przykład dotyczy kompilatorów i tym podobnych. Streszczenie drzewa składni (AST) może dokładnie zdefiniować strukturę języka programowania, ale operacje, które możesz chcieć wykonywać na AST, zmienią się wraz z postępem projektu: generatory kodów, ładne drukarki, debuggery, analiza metryk złożoności.

Bez wzorca gościa za każdym razem, gdy programista chciał dodać nową funkcję, musiałby dodać tę metodę do każdej funkcji w klasie podstawowej. Jest to szczególnie trudne, gdy klasy podstawowe pojawiają się w osobnej bibliotece lub są tworzone przez oddzielny zespół.

(Słyszałem, że argumentował, że wzorzec gościa jest w konflikcie z dobrymi praktykami OO, ponieważ odsuwa operacje danych od danych. Wzorzec gościa jest przydatny w sytuacji, w której zawodzą normalne praktyki OO).


Chciałbym również poznać Twoją opinię na następujące tematy: Dlaczego miałbym używać wzorca odwiedzających tylko klasę użyteczności. mogę wywołać moją klasę narzędziową w następujący sposób: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Co za różnica ? oboje robią oddzielne obawy, prawda? mam nadzieję, że możesz pomóc.
j2emanue

@ j2emanue: Nie rozumiem pytania. Sugeruję, abyś wypełnił go i opublikował jako pełne pytanie, na które każdy może odpowiedzieć.
Dziwne,

1
opublikowałem nowe pytanie tutaj: stackoverflow.com/questions/52068876/...
j2emanue

14

Istnieją co najmniej trzy bardzo dobre powody, aby używać Wzorca Odwiedzającego:

  1. Ograniczaj rozprzestrzenianie się kodu, który tylko nieznacznie różni się, gdy zmieniają się struktury danych.

  2. Zastosuj to samo obliczenie do kilku struktur danych, bez zmiany kodu, który implementuje obliczenia.

  3. Dodaj informacje do starszych bibliotek bez zmiany starszego kodu.

Proszę spojrzeć na artykuł, który o tym napisałem .


1
Skomentowałem twój artykuł z jednym największym zastosowaniem, jaki widziałem dla odwiedzających. Myśli?
George Mauer,

13

Jak już zauważył Konrad Rudolph, nadaje się do przypadków, w których potrzebujemy podwójnej wysyłki

Oto przykład pokazujący sytuację, w której potrzebujemy podwójnej wysyłki i jak gość pomaga nam w tym.

Przykład:

Powiedzmy, że mam 3 rodzaje urządzeń mobilnych - iPhone, Android, Windows Mobile.

Wszystkie te trzy urządzenia mają zainstalowane radio Bluetooth.

Załóżmy, że radio z niebieskim zębem może pochodzić od 2 oddzielnych producentów OEM - Intel i Broadcom.

Aby uczynić przykład odpowiednim dla naszej dyskusji, załóżmy również, że interfejsy API udostępniane przez radio Intela różnią się od tych udostępnianych przez radio Broadcom.

Tak wyglądają moje zajęcia -

wprowadź opis zdjęcia tutaj wprowadź opis zdjęcia tutaj

Teraz chciałbym wprowadzić operację - Włączanie Bluetooth na urządzeniu mobilnym.

Podpis funkcji powinien polubić coś takiego -

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

Tak więc w zależności od odpowiedniego rodzaju urządzenia i w zależności od odpowiedniego rodzaju radia Bluetooth można go włączyć, wywołując odpowiednie kroki lub algorytm .

Zasadniczo staje się matrycą 3 x 2, gdzie staram się wektorować właściwą operację w zależności od odpowiedniego rodzaju obiektów.

Zachowanie polimorficzne w zależności od rodzaju obu argumentów.

wprowadź opis zdjęcia tutaj

Teraz do tego problemu można zastosować wzór użytkownika. Inspiracja pochodzi ze strony Wikipedii: „Zasadniczo odwiedzający pozwala dodawać nowe funkcje wirtualne do rodziny klas bez modyfikowania samych klas; zamiast tego tworzy się klasę gości, która implementuje wszystkie odpowiednie specjalizacje funkcji wirtualnej. Gość przyjmuje referencję instancji jako dane wejściowe i realizuje cel poprzez podwójną wysyłkę. ”

Podwójna wysyłka jest tutaj konieczna ze względu na matrycę 3x2

Oto jak będzie wyglądać konfiguracja - wprowadź opis zdjęcia tutaj

Napisałem przykład, aby odpowiedzieć na inne pytanie, kod i jego wyjaśnienie jest tutaj wymienione .


9

Łatwiej mi było znaleźć następujące linki:

W http://www.remondo.net/visitor-pattern-example-csharp/ znalazłem przykład, który pokazuje próbny przykład, który pokazuje, jakie są zalety wzorca odwiedzin. Tutaj masz różne klasy kontenerów dla Pill:

namespace DesignPatterns
{
    public class BlisterPack
    {
        // Pairs so x2
        public int TabletPairs { get; set; }
    }

    public class Bottle
    {
        // Unsigned
        public uint Items { get; set; }
    }

    public class Jar
    {
        // Signed
        public int Pieces { get; set; }
    }
}

Jak widać powyżej, BilsterPackzawierasz pary Pigułek, więc musisz pomnożyć liczbę par przez 2. Możesz także zauważyć, że Bottleużycie unitjest innym typem danych i trzeba je rzucić.

Tak więc w głównej metodzie możesz obliczyć liczbę pigułek za pomocą następującego kodu:

foreach (var item in packageList)
{
    if (item.GetType() == typeof (BlisterPack))
    {
        pillCount += ((BlisterPack) item).TabletPairs * 2;
    }
    else if (item.GetType() == typeof (Bottle))
    {
        pillCount += (int) ((Bottle) item).Items;
    }
    else if (item.GetType() == typeof (Jar))
    {
        pillCount += ((Jar) item).Pieces;
    }
}

Zauważ, że powyższy kod narusza Single Responsibility Principle. Oznacza to, że musisz zmienić główny kod metody, jeśli dodasz nowy typ kontenera. Wydłużanie czasu przełączania jest również złą praktyką.

Więc wprowadzając następujący kod:

public class PillCountVisitor : IVisitor
{
    public int Count { get; private set; }

    #region IVisitor Members

    public void Visit(BlisterPack blisterPack)
    {
        Count += blisterPack.TabletPairs * 2;
    }

    public void Visit(Bottle bottle)
    {
        Count += (int)bottle.Items;
    }

    public void Visit(Jar jar)
    {
        Count += jar.Pieces;
    }

    #endregion
}

Przeniosłeś odpowiedzialność za zliczanie liczby Pills do klasy o nazwie PillCountVisitor(I usunęliśmy instrukcję case switch). Oznacza to, że ilekroć musisz dodać nowy typ pojemnika na pigułki, powinieneś zmienić tylko PillCountVisitorklasę. Zauważ też, że IVisitorinterfejs jest ogólny do użycia w innych scenariuszach.

Dodając metodę Accept do klasy pojemnika na pigułki:

public class BlisterPack : IAcceptor
{
    public int TabletPairs { get; set; }

    #region IAcceptor Members

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    #endregion
}

pozwalamy odwiedzającym odwiedzać klasy pojemników na pigułki.

Na koniec obliczamy liczbę tabletek za pomocą następującego kodu:

var visitor = new PillCountVisitor();

foreach (IAcceptor item in packageList)
{
    item.Accept(visitor);
}

To znaczy: każdy pojemnik na pigułki pozwala PillCountVisitorodwiedzającemu zobaczyć, jak liczą się jego pigułki. On wie, jak liczyć pigułki.

U visitor.Countma wartość pigułek.

W http://butunclebob.com/ArticleS.UncleBob.IuseVisitor widzisz prawdziwy scenariusz, w którym nie możesz użyć polimorfizmu (odpowiedzi), aby przestrzegać zasady pojedynczej odpowiedzialności. W rzeczywistości w:

public class HourlyEmployee extends Employee {
  public String reportQtdHoursAndPay() {
    //generate the line for this hourly employee
  }
}

reportQtdHoursAndPaymetoda służy do informowania i reprezentacji, a to naruszać jednolitego odpowiedzialność zasady. Dlatego lepiej jest użyć wzorca odwiedzającego, aby rozwiązać problem.


2
Cześć Sayed, czy możesz edytować swoją odpowiedź, aby dodać części, które uważasz za najbardziej pouczające. Zasadniczo SO odradza odpowiedzi tylko z linkami, ponieważ celem jest baza danych wiedzy i linki się psują.
George Mauer,

8

Podwójna wysyłka jest tylko jednym z powodów, dla których warto skorzystać z tego wzoru .
Należy jednak pamiętać, że jest to jeden sposób na wdrożenie podwójnej lub większej liczby wysyłek w językach korzystających z paradygmatu pojedynczej wysyłki.

Oto powody, dla których warto użyć wzorca:

1) Chcemy definiować nowe operacje bez zmiany modelu za każdym razem, ponieważ model nie zmienia się często operacje zmieniania często.

2) Nie chcemy łączyć modelu z zachowaniem, ponieważ chcemy mieć model wielokrotnego użytku w wielu aplikacjach lub chcemy mieć rozszerzalny model, który pozwala klasom klientów definiować ich zachowania za pomocą własnych klas.

3) Mamy wspólne operacje, które zależą od konkretnego typu modelu, ale nie chcemy implementować logiki w każdej podklasie, ponieważ rozłożyłoby to logikę na wiele klas, a więc w wielu miejscach .

4) Używamy projektu modelu domeny, a klasy modeli o tej samej hierarchii wykonują zbyt wiele różnych rzeczy, które można by zebrać gdzie indziej .

5) Potrzebujemy podwójnej wysyłki .
Mamy zmienne zadeklarowane z typami interfejsów i chcemy móc je przetwarzać zgodnie z typem środowiska wykonawczego… oczywiście bez użycia if (myObj instanceof Foo) {}żadnej sztuczki.
Pomysł polega na przykład na przekazaniu tych zmiennych do metod, które deklarują konkretny typ interfejsu jako parametr do zastosowania określonego przetwarzania. Ten sposób działania nie jest możliwy po wyjęciu z pudełka, ponieważ języki zależą od pojedynczej wysyłki, ponieważ wybrana funkcja wywoływana w czasie wykonywania zależy tylko od typu środowiska uruchomieniowego odbiornika.
Zauważ, że w Javie metoda (podpis) do wywołania jest wybierana w czasie kompilacji i zależy od zadeklarowanego typu parametrów, a nie ich typu środowiska wykonawczego.

Ostatni punkt, który jest powodem do korzystania z gościa, jest również konsekwencją, ponieważ podczas implementowania gościa (oczywiście w przypadku języków, które nie obsługują wielokrotnej wysyłki), koniecznie trzeba wprowadzić implementację podwójnej wysyłki.

Pamiętaj, że przejście elementów (iteracja) w celu zastosowania użytkownika na każdym z nich nie jest powodem do użycia wzoru.
Używasz wzorca, ponieważ dzielisz model i przetwarzanie.
Korzystając ze wzoru, zyskujesz dodatkowo na zdolności iteratora.
Ta umiejętność jest bardzo potężna i wykracza poza iterację na typie zwykłym z określoną metodą, podobnie jak accept()metoda ogólna.
Jest to specjalny przypadek użycia. Odłożę to na bok.


Przykład w Javie

Zilustruję wartość dodaną wzoru na przykładzie szachów, w którym chcielibyśmy zdefiniować przetwarzanie, gdy gracz zażąda ruchu elementu.

Bez użycia wzorca odwiedzającego moglibyśmy zdefiniować zachowania związane z przemieszczaniem elementów bezpośrednio w podklasach elementów.
Możemy mieć na przykład Pieceinterfejs taki jak:

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

Każda podklasa Piece zaimplementuje to, na przykład:

public class Pawn implements Piece{

    @Override
    public boolean checkMoveValidity(Coordinates coord) {
        ...
    }

    @Override
    public void performMove(Coordinates coord) {
        ...
    }

    @Override
    public Piece computeIfKingCheck() {
        ...
    }

}

To samo dotyczy wszystkich podklas Piece.
Oto klasa diagramów ilustrująca ten projekt:

[schemat klas modeli

Takie podejście ma trzy ważne wady:

- zachowania takie jak performMove()lub computeIfKingCheck()najprawdopodobniej wykorzystają wspólną logikę.
Na przykład, bez względu na konkretny Piece, performMove()ostatecznie ustawi bieżący element w określonym miejscu i potencjalnie zabierze element przeciwnika.
Podział pokrewnych zachowań na wiele klas zamiast gromadzenia ich, w pewien sposób pokonuje pojedynczy wzorzec odpowiedzialności. Utrudniając ich konserwację.

- przetwarzanie, które checkMoveValidity()nie powinno być czymś, co Piecepodklasy mogą zobaczyć lub zmienić.
Jest to czek wykraczający poza działania człowieka lub komputera. Ta kontrola jest wykonywana przy każdej akcji żądanej przez gracza, aby upewnić się, że żądany ruch pionka jest prawidłowy.
Więc nawet nie chcemy tego podawać w Pieceinterfejsie.

- W grach szachowych stanowiących wyzwanie dla twórców botów, ogólnie aplikacja zapewnia standardowy interfejs API ( Pieceinterfejsy, podklasy, planszowe, wspólne zachowania itp.) I pozwala programistom wzbogacić swoją strategię botów.
Aby to zrobić, musimy zaproponować model, w którym dane i zachowania nie są ściśle powiązane z Pieceimplementacjami.

Przejdźmy więc do wzorca odwiedzin!

Mamy dwa rodzaje struktur:

- wzorcowe klasy, które akceptują zwiedzanie (sztuki)

- odwiedzający je odwiedzający (przeprowadzki)

Oto schemat klas ilustrujący wzorzec:

wprowadź opis zdjęcia tutaj

W górnej części mamy gości, aw dolnej mamy klasy modeli.

Oto PieceMovingVisitorinterfejs (zachowanie określone dla każdego rodzaju Piece):

public interface PieceMovingVisitor {

    void visitPawn(Pawn pawn);

    void visitKing(King king);

    void visitQueen(Queen queen);

    void visitKnight(Knight knight);

    void visitRook(Rook rook);

    void visitBishop(Bishop bishop);

}

Kawałek jest teraz zdefiniowany:

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

Jego kluczową metodą jest:

void accept(PieceMovingVisitor pieceVisitor);

Zapewnia pierwszą wysyłkę: wywołanie na podstawie Pieceodbiorcy.
W czasie kompilacji metoda jest powiązana z accept()metodą interfejsu Piece, aw czasie wykonywania metoda ograniczona zostanie wywołana w Pieceklasie środowiska wykonawczego .
I to accept()implementacja metody wykona drugą wysyłkę.

Rzeczywiście, każda Piecepodklasa, która chce być odwiedzana przez PieceMovingVisitorobiekt, wywołuje PieceMovingVisitor.visit()metodę, przekazując jako sam argument.
W ten sposób kompilator ogranicza, jak tylko czas kompilacji, typ zadeklarowanego parametru o typie konkretnym.
Jest druga wysyłka.
Oto Bishoppodklasa, która ilustruje to:

public class Bishop implements Piece {

    private Coordinates coord;

    public Bishop(Coordinates coord) {
        super(coord);
    }

    @Override
    public void accept(PieceMovingVisitor pieceVisitor) {
        pieceVisitor.visitBishop(this);
    }

    @Override
    public Coordinates getCoordinates() {
        return coordinates;
    }

   @Override
    public void setCoordinates(Coordinates coordinates) {
        this.coordinates = coordinates;
   }

}

A tutaj przykład użycia:

// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();

// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);

// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
    piece.accept(new MovePerformingVisitor(coord));
}

Wady odwiedzających

Wzorzec gościa jest bardzo silnym wzorcem, ale ma również pewne ważne ograniczenia, które należy rozważyć przed użyciem.

1) Ryzyko zmniejszenia / zerwania kapsułkowania

W niektórych rodzajach operacji wzorzec użytkownika może zmniejszyć lub przerwać enkapsulację obiektów domeny.

Na przykład, ponieważ MovePerformingVisitor klasa musi ustawić współrzędne rzeczywistego elementu, Pieceinterfejs musi zapewnić sposób, aby to zrobić:

void setCoordinates(Coordinates coordinates);

Odpowiedzialność za Piecezmiany współrzędnych jest teraz otwarta dla innych klas niż Piecepodklasy.
Przeniesienie przetwarzania wykonywanego przez gościa w Piecepodklasach również nie jest opcją.
To rzeczywiście stworzy inny problem, ponieważ Piece.accept()akceptuje każdą implementację odwiedzającego. Nie wie, co robi użytkownik, więc nie ma pojęcia o tym, czy i jak zmienić stan Piece.
Sposobem na identyfikację gościa byłoby wykonanie przetwarzania końcowego Piece.accept()zgodnie z implementacją gościa. Byłoby to bardzo zły pomysł, gdyż powodowałoby duże sprzężenie pomiędzy implementacjami gościem i podklasy kawałku i oprócz to prawdopodobnie wymagać, aby użyć jako sztuczkę getClass(), instanceoflub dowolny znacznik określający realizację odwiedzających.

2) Wymóg zmiany modelu

W przeciwieństwie do niektórych innych wzorców behawioralnych, takich jak Decoratorna przykład wzorzec odwiedzających jest nachalny.
Rzeczywiście musimy zmodyfikować początkową klasę odbiornika, aby zapewnić accept()metodę akceptacji odwiedzin.
Nie mieliśmy żadnych problemów Piecei podklas, ponieważ są to nasze klasy .
W przypadku klas wbudowanych lub zajęć dla osób trzecich sprawy nie są takie proste.
Musimy je zawinąć lub odziedziczyć (jeśli możemy), aby dodać accept()metodę.

3) Kierunki

Wzór tworzy wielokrotność pośrednich.
Podwójna wysyłka oznacza dwie inwokacje zamiast jednej:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor)

I możemy mieć dodatkowe pośrednie, gdy gość zmienia stan odwiedzanego obiektu.
Może to wyglądać jak cykl:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)

6

Cay Horstmann ma świetny przykład zastosowania Odwiedzającego w swojej książce projektowej i wzorach OO . Podsumowuje problem:

Obiekty złożone często mają złożoną strukturę, złożoną z pojedynczych elementów. Niektóre elementy mogą ponownie zawierać elementy potomne. ... Operacja na elemencie odwiedza jego elementy potomne, stosuje wobec nich operację i łączy wyniki. ... Jednak dodawanie nowych operacji do takiego projektu nie jest łatwe.

Nie jest to łatwe, ponieważ operacje są dodawane w ramach samych klas struktur. Na przykład wyobraź sobie, że masz system plików:

Schemat klasy FileSystem

Oto niektóre operacje (funkcjonalności), które moglibyśmy chcieć wdrożyć za pomocą tej struktury:

  • Wyświetl nazwy elementów węzła (lista plików)
  • Wyświetl obliczoną wielkość elementów węzła (gdzie rozmiar katalogu obejmuje rozmiar wszystkich jego elementów potomnych)
  • itp.

Możesz dodać funkcje do każdej klasy w systemie plików, aby zaimplementować operacje (a ludzie robili to w przeszłości, ponieważ to bardzo oczywiste, jak to zrobić). Problem polega na tym, że za każdym razem, gdy dodajesz nową funkcjonalność (wiersz „itd.” Powyżej), może być konieczne dodawanie coraz większej liczby metod do klas struktur. W pewnym momencie, po pewnej liczbie operacji dodanych do oprogramowania, metody w tych klasach nie mają już sensu pod względem spójności funkcjonalnej klas. Na przykład masz FileNodemetodę, która ma calculateFileColorForFunctionABC()zaimplementować najnowszą funkcjonalność wizualizacji w systemie plików.

Wzorzec gościa (podobnie jak wiele wzorców projektowych) narodził się z bólu i cierpienia programistów, którzy wiedzieli, że istnieje lepszy sposób na zmianę kodu bez konieczności wprowadzania wielu zmian wszędzie, a także z poszanowaniem dobrych zasad projektowania (wysoka spójność, niskie sprzężenie) ). Moim zdaniem, trudno jest zrozumieć przydatność wielu wzorów, dopóki nie poczujesz tego bólu. Wyjaśnienie bólu (tak jak próbujemy to zrobić powyżej za pomocą dodanych funkcji „itp.”) Zajmuje miejsce w wyjaśnieniu i jest rozproszeniem. Z tego powodu zrozumienie wzorców jest trudne.

Visitor pozwala nam oddzielić funkcje w strukturze danych (np. FileSystemNodes) Od samych struktur danych. Wzorzec pozwala projektowi zachować spójność - klasy struktur danych są prostsze (mają mniej metod), a także funkcjonalności są zawarte w Visitorimplementacjach. Odbywa się to poprzez podwójne wywoływanie (co jest skomplikowaną częścią wzorca): przy użyciu accept()metod w klasach struktur i visitX()metod w klasach Visitor (funkcjonalność):

Schemat klasy FileSystem z zastosowanym gościem

Ta struktura pozwala nam dodawać nowe funkcjonalności, które działają na strukturze jako konkretne osoby odwiedzające (bez zmiany klas struktur).

Schemat klasy FileSystem z zastosowanym gościem

Na przykład: a, PrintNameVisitorktóry implementuje funkcję wyświetlania katalogu, a a, PrintSizeVisitorktóry implementuje wersję o rozmiarze. Możemy sobie wyobrazić, że pewnego dnia mamy „ExportXMLVisitor”, który generuje dane w formacie XML, lub innego użytkownika, który generuje je w JSON, itp. Moglibyśmy nawet mieć gościa, który wyświetla moje drzewo katalogów za pomocą języka graficznego takiego jak DOT , do wizualizacji z innym programem.

Na koniec: złożoność Visora ​​z podwójną wysyłką oznacza, że ​​trudniej jest go zrozumieć, zakodować i debugować. Krótko mówiąc, ma wysoki współczynnik maniaka i idzie w parze z zasadą KISS. W ankiecie przeprowadzonej przez badaczy, Visitor wykazał kontrowersyjny wzorzec (nie było zgody co do jego przydatności). Niektóre eksperymenty wykazały nawet, że nie ułatwia to utrzymania kodu.


Myślę, że struktura katalogów jest dobrym wzorcem złożonym, ale zgadza się z ostatnim akapitem.
zar

5

Moim zdaniem nakład pracy związany z dodaniem nowej operacji jest mniej więcej taki sam przy użyciu Visitor Patternlub bezpośredniej modyfikacji struktury każdego elementu. Ponadto, jeśli miałbym dodać nową klasę elementów, powiedzmy Cow, wpłynie to na interfejs Operacji, co spowoduje propagację do wszystkich istniejących klas elementów, co wymaga ponownej kompilacji wszystkich klas elementów. Więc o co chodzi?


4
Niemal za każdym razem, gdy korzystam z narzędzia Visitor, pracujesz nad przemierzaniem hierarchii obiektów. Rozważ menu zagnieżdżonego drzewa. Chcesz zwinąć wszystkie węzły. Jeśli nie wdrożysz gościa, musisz napisać kod przejścia przez wykres. Lub odwiedzającego: rootElement.visit (node) -> node.collapse(). Z gościem, każdy węzeł implementuje przechodzenie wykresu dla wszystkich swoich dzieci, więc gotowe.
George Mauer,

@GeorgeMauer, koncepcja podwójnej wysyłki wyjaśniła mi motywację: logika zależna od typu dotyczy rodzaju lub świata bólu. Pomysł dystrybucji logiki przechodzenia wciąż mnie zatrzymuje. Czy to jest bardziej wydajne? Czy to jest łatwiejsze w utrzymaniu? Co się stanie, jeśli wymagane jest dodanie opcji „fold to level N”?
nik.shornikov

Wydajność @ nik.shornikov naprawdę nie powinna być tutaj problemem. W prawie każdym języku kilka wywołań funkcji jest nieistotnym narzutem. Poza tym jest to mikrooptymalizacja. Czy to jest łatwiejsze w utrzymaniu? Cóż, to zależy. Myślę, że większość razy tak jest, czasem tak nie jest. Jeśli chodzi o „fold do poziomu N”. Łatwe podanie w levelsRemainingliczniku jako parametr. Zmniejsz to przed wywołaniem następnego poziomu dzieci. Wewnątrz twojego gościa if(levelsRemaining == 0) return.
George Mauer,

1
@GeorgeMauer, całkowicie zgodził się, że wydajność jest niewielkim problemem. Ale łatwość utrzymania, np. Przesłanianie podpisu akceptacji, jest dokładnie tym, co moim zdaniem powinna sprowadzać się do decyzji.
nik.shornikov

5

Wzorzec gościa jako ta sama podziemna implementacja programowania obiektów Aspect ..

Na przykład, jeśli zdefiniujesz nową operację bez zmiany klas elementów, na których ona działa


do wzmianki o programowaniu obiektów
aspektowych

5

Krótki opis wzoru odwiedzającego. Wszystkie klasy wymagające modyfikacji muszą implementować metodę „accept”. Klienci nazywają tę metodę akceptacji, aby wykonać nowe działanie na tej rodzinie klas, rozszerzając w ten sposób swoją funkcjonalność. Klienci mogą skorzystać z tej jednej metody akceptacji, aby wykonać szeroki zakres nowych akcji, przekazując inną klasę odwiedzających dla każdej konkretnej akcji. Klasa odwiedzających zawiera wiele przesłoniętych metod odwiedzin definiujących, jak osiągnąć to samo działanie dla każdej klasy w rodzinie. Te metody odwiedzin otrzymują instancję, na której można pracować.

Kiedy możesz rozważyć jego użycie

  1. Kiedy masz rodzinę klas, wiesz, że będziesz musiał dodać wiele nowych akcji, ale z jakiegoś powodu nie jesteś w stanie zmienić ani ponownie skompilować rodziny klas w przyszłości.
  2. Jeśli chcesz dodać nową akcję i mieć ją całkowicie zdefiniowaną w ramach jednej klasy odwiedzających, a nie rozłożyć ją na wiele klas.
  3. Kiedy twój szef mówi, że musisz stworzyć szereg klas, które muszą teraz coś zrobić ! ... ale nikt tak naprawdę nie wie dokładnie, co to jest.

4

Nie zrozumiałem tego wzoru, dopóki nie natknąłem się na artykuł wuja boba i nie przeczytałem komentarzy. Rozważ następujący kod:

public class Employee
{
}

public class SalariedEmployee : Employee
{
}

public class HourlyEmployee : Employee
{
}

public class QtdHoursAndPayReport
{
    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        foreach (Employee e in employees)
        {
            if (e is HourlyEmployee he)
                PrintReportLine(he);
            if (e is SalariedEmployee se)
                PrintReportLine(se);
        }
    }

    public void PrintReportLine(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hours");
    }
    public void PrintReportLine(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    }
}

class Program
{
    static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }
}

Choć może wyglądać dobrze, ponieważ potwierdza Pojedynczą Odpowiedzialność , narusza zasadę Otwartej / Zamkniętej . Za każdym razem, gdy masz nowy typ pracownika, będziesz musiał go dodać, jeśli sprawdzasz typ. A jeśli tego nie zrobisz, nigdy nie dowiesz się tego podczas kompilacji.

Dzięki wzorowi odwiedzających możesz uczynić swój kod czystszym, ponieważ nie narusza zasady otwartej / zamkniętej i nie narusza Pojedynczej odpowiedzialności. A jeśli zapomnisz zaimplementować wizytę, nie zostanie ona skompilowana:

public abstract class Employee
{
    public abstract void Accept(EmployeeVisitor v);
}

public class SalariedEmployee : Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public class HourlyEmployee:Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public interface EmployeeVisitor
{
    void Visit(HourlyEmployee he);
    void Visit(SalariedEmployee se);
}

public class QtdHoursAndPayReport : EmployeeVisitor
{
    public void Visit(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hourly");
        // generate the line of the report.
    }
    public void Visit(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    } // do nothing

    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        QtdHoursAndPayReport v = new QtdHoursAndPayReport();
        foreach (var emp in employees)
        {
            emp.Accept(v);
        }
    }
}

class Program
{

    public static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }       
}  
}

Magia polega na tym, że chociaż v.Visit(this)wygląda tak samo, w rzeczywistości jest inna, ponieważ wywołuje różne przeciążenia odwiedzających.


Tak, szczególnie uważam, że jest to przydatne podczas pracy ze strukturami drzew, a nie tylko płaskimi listami (płaskie listy byłyby szczególnym przypadkiem drzewa). Jak zauważyłeś, nie jest strasznie niechlujny tylko na listach, ale gość może być wybawicielem, ponieważ nawigacja między węzłami staje się bardziej złożona
George Mauer

3

Na podstawie doskonałej odpowiedzi @Federico A. Ramponi.

Wyobraź sobie, że masz tę hierarchię:

public interface IAnimal
{
    void DoSound();
}

public class Dog : IAnimal
{
    public void DoSound()
    {
        Console.WriteLine("Woof");
    }
}

public class Cat : IAnimal
{
    public void DoSound(IOperation o)
    {
        Console.WriteLine("Meaw");
    }
}

Co się stanie, jeśli musisz tutaj dodać metodę „Spacer”? Będzie to bolesne dla całego projektu.

Jednocześnie dodanie metody „Spacer” generuje nowe pytania. Co z „jedzeniem” lub „snem”? Czy naprawdę musimy dodawać nową metodę do hierarchii Zwierząt dla każdej nowej akcji lub operacji, którą chcemy dodać? To brzydkie i najważniejsze, nigdy nie będziemy w stanie zamknąć interfejsu Zwierząt. Dzięki wzorowi użytkowników możemy dodawać nową metodę do hierarchii bez modyfikowania hierarchii!

Więc po prostu sprawdź i uruchom ten przykład w C #:

using System;
using System.Collections.Generic;

namespace VisitorPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var animals = new List<IAnimal>
            {
                new Cat(), new Cat(), new Dog(), new Cat(), 
                new Dog(), new Dog(), new Cat(), new Dog()
            };

            foreach (var animal in animals)
            {
                animal.DoOperation(new Walk());
                animal.DoOperation(new Sound());
            }

            Console.ReadLine();
        }
    }

    public interface IOperation
    {
        void PerformOperation(Dog dog);
        void PerformOperation(Cat cat);
    }

    public class Walk : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Dog walking");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Cat Walking");
        }
    }

    public class Sound : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Woof");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Meaw");
        }
    }

    public interface IAnimal
    {
        void DoOperation(IOperation o);
    }

    public class Dog : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }

    public class Cat : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }
}

chodzić, jeść nie są odpowiednimi przykładami, ponieważ są one wspólne zarówno dla, Dogjak i dla Cat. Mogłeś zrobić je w klasie podstawowej, aby były dziedziczone lub wybrać odpowiedni przykład.
Abhinav Gauniyal,

dźwięki są różne, dobra próbka, ale nie wiadomo, czy ma to coś wspólnego ze schematem odwiedzin
DAG

3

Gość

Visitor pozwala dodawać nowe funkcje wirtualne do rodziny klas bez modyfikowania samych klas; zamiast tego tworzy się klasę gości, która implementuje wszystkie odpowiednie specjalizacje funkcji wirtualnej

Struktura odwiedzających:

wprowadź opis zdjęcia tutaj

Użyj wzorca użytkownika, jeśli:

  1. Podobne operacje należy wykonać na obiektach różnych typów zgrupowanych w strukturze
  2. Musisz wykonać wiele różnych i niepowiązanych operacji. Oddziela Operację od struktury obiektów
  3. Nowe operacje muszą zostać dodane bez zmiany struktury obiektu
  4. Zbierz powiązane operacje w jedną klasę, zamiast zmuszać cię do zmiany lub wyprowadzenia klas
  5. Dodaj funkcje do bibliotek klas, dla których albo nie masz źródła, albo nie możesz go zmienić

Chociaż Visitor wzór zapewnia elastyczność, aby dodać nową operację bez zmiany istniejącego kodu w obiekcie, elastyczność ta ma pochodzić z wadą.

Jeśli dodano nowy obiekt możliwy do zwiedzania, wymaga on zmiany kodu w klasach Visitor i ConcreteVisitor . Istnieje sposób obejścia tego problemu: Użyj refleksji, która wpłynie na wydajność.

Fragment kodu:

import java.util.HashMap;

interface Visitable{
    void accept(Visitor visitor);
}

interface Visitor{
    void logGameStatistics(Chess chess);
    void logGameStatistics(Checkers checkers);
    void logGameStatistics(Ludo ludo);    
}
class GameVisitor implements Visitor{
    public void logGameStatistics(Chess chess){
        System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");    
    }
    public void logGameStatistics(Checkers checkers){
        System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");    
    }
    public void logGameStatistics(Ludo ludo){
        System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");    
    }
}

abstract class Game{
    // Add game related attributes and methods here
    public Game(){

    }
    public void getNextMove(){};
    public void makeNextMove(){}
    public abstract String getName();
}
class Chess extends Game implements Visitable{
    public String getName(){
        return Chess.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Checkers extends Game implements Visitable{
    public String getName(){
        return Checkers.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Ludo extends Game implements Visitable{
    public String getName(){
        return Ludo.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}

public class VisitorPattern{
    public static void main(String args[]){
        Visitor visitor = new GameVisitor();
        Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
        for (Visitable v : games){
            v.accept(visitor);
        }
    }
}

Wyjaśnienie:

  1. Visitable( Element) jest interfejsem i tę metodę interfejsu należy dodać do zestawu klas.
  2. Visitorto interfejs zawierający metody wykonywania operacji na Visitableelementach.
  3. GameVisitorjest klasą, która implementuje Visitorinterface ( ConcreteVisitor).
  4. Każdy Visitableelement akceptuje Visitori wywołuje odpowiednią metodę Visitorinterfejsu.
  5. Można traktować Gamejak Elementi konkretne gry, takie jak Chess,Checkers and Ludojak ConcreteElements.

W powyższym przykładzie Chess, Checkers and Ludosą trzy różne gry (i Visitableklasy). Pewnego pięknego dnia napotkałem scenariusz rejestrowania statystyk każdej gry. Zatem bez modyfikowania poszczególnych klas w celu zaimplementowania funkcji statystycznych możesz scentralizować tę odpowiedzialność w GameVisitorklasie, co załatwi sprawę bez modyfikowania struktury każdej gry.

wynik:

Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser

Odnosić się do

artykuł oodesign

artykuł o zaopatrzeniu

po więcej szczegółów

Dekorator

wzorzec pozwala na dodanie zachowania do pojedynczego obiektu, statycznie lub dynamicznie, bez wpływu na zachowanie innych obiektów z tej samej klasy

Powiązane posty:

Wzór dekoratora dla IO

Kiedy stosować wzór dekoratora?


2

Bardzo podoba mi się opis i przykład z http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html .

Zakłada się, że masz ustaloną hierarchię klas podstawowych; być może pochodzi od innego dostawcy i nie można wprowadzać zmian w tej hierarchii. Jednak Twoim celem jest dodanie nowych metod polimorficznych do tej hierarchii, co oznacza, że ​​normalnie musisz coś dodać do interfejsu klasy podstawowej. Dylemat polega na tym, że musisz dodać metody do klasy podstawowej, ale nie możesz dotknąć klasy podstawowej. Jak sobie z tym poradzić?

Wzorzec projektowy, który rozwiązuje tego rodzaju problem, nazywa się „gościem” (ostatni w książce Wzory projektowe) i opiera się na schemacie podwójnej wysyłki pokazanym w ostatniej części.

Wzorzec gościa pozwala rozszerzyć interfejs typu podstawowego, tworząc osobną hierarchię klas typu Visitor, aby zwirtualizować operacje wykonywane na typie podstawowym. Obiekty typu podstawowego po prostu „akceptują” gościa, a następnie wywołują dynamicznie powiązaną funkcję członka.


Chociaż technicznie wzorzec Visitor jest tak naprawdę tylko podstawową podwójną wysyłką z ich przykładu. Twierdziłbym, że sama użyteczność nie jest szczególnie widoczna.
George Mauer,

1

Chociaż rozumiem, jak i kiedy, nigdy nie zrozumiałem, dlaczego. Jeśli pomaga to każdemu, kto ma doświadczenie w języku takim jak C ++, chcesz przeczytać to bardzo uważnie.

Dla leniwych używamy wzorca odwiedzin, ponieważ „podczas gdy funkcje wirtualne są wywoływane dynamicznie w C ++, przeciążanie funkcji odbywa się statycznie” .

Lub, inaczej mówiąc, aby upewnić się, że CollideWith (ApolloSpacecraft &) jest wywoływany, gdy przechodzisz w odniesieniu do statku kosmicznego, który jest faktycznie związany z obiektem ApolloSpacecraft.

class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
  virtual void CollideWith(SpaceShip&) {
    cout << "ExplodingAsteroid hit a SpaceShip" << endl;
  }
  virtual void CollideWith(ApolloSpacecraft&) {
    cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
  }
}

2
Zastosowanie dynamicznej wysyłki w strukturze odwiedzających całkowicie mnie dziwi. Sugerowane zastosowania wzorca opisują rozgałęzienia, które można wykonać w czasie kompilacji. Wydaje się, że te przypadki byłyby lepiej z szablonem funkcji.
Praxeolitic,

0

Dzięki za niesamowite wyjaśnienie @Federico A. Ramponi , właśnie to zrobiłem w wersji java . Mam nadzieję, że to może być pomocne.

Również, jak zauważył @Konrad Rudolph , tak naprawdę jest to podwójna wysyłka z wykorzystaniem dwóch konkretnych instancji razem w celu określenia metod działania.

Tak więc właściwie nie ma potrzeby tworzenia wspólnego interfejsu dla executora operacji, o ile mamy poprawnie zdefiniowany interfejs operacji .

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showTheHobby(food);
        Katherine katherine = new Katherine();
        katherine.presentHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void embed(Katherine katherine);
}


class Hearen {
    String name = "Hearen";
    void showTheHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine {
    String name = "Katherine";
    void presentHobby(Hobby hobby) {
        hobby.embed(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void embed(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

Jak można się spodziewać, wspólny interfejs zapewni nam większą przejrzystość, choć tak naprawdę nie jest to niezbędna część tego wzorca.

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showHobby(food);
        Katherine katherine = new Katherine();
        katherine.showHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void insert(Katherine katherine);
}

abstract class Person {
    String name;
    protected Person(String n) {
        this.name = n;
    }
    abstract void showHobby(Hobby hobby);
}

class Hearen extends  Person {
    public Hearen() {
        super("Hearen");
    }
    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine extends Person {
    public Katherine() {
        super("Katherine");
    }

    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void insert(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

0

twoje pytanie brzmi: kiedy wiedzieć:

nie najpierw koduję według wzorca odwiedzającego. koduję standard i czekam na konieczność wystąpienia, a następnie refaktoryzuję. powiedzmy, że masz wiele systemów płatności, które zainstalowałeś jednocześnie. W kasie możesz mieć wiele warunków if (lub instanceOf), na przykład:

//psuedo code
    if(payPal) 
    do paypal checkout 
    if(stripe)
    do strip stuff checkout
    if(payoneer)
    do payoneer checkout

teraz wyobraź sobie, że mam 10 metod płatności, robi się trochę brzydko. Więc kiedy zobaczysz, że taki wzorzec występujący w odwiedzinach przydaje się, aby oddzielić to wszystko, a potem w końcu wywołujesz coś takiego:

new PaymentCheckoutVistor(paymentType).visit()

Możesz zobaczyć, jak to zaimplementować na podstawie wielu przykładów tutaj, po prostu pokazuję ci przypadek użycia.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.