Quando dovrei usare il pattern di progettazione dei visitatori?

Continuo a vedere i riferimenti al modello dei visitatori nei blog, ma devo ammettere che, semplicemente, non capisco. Ho letto l’ articolo di wikipedia per il pattern e capisco la sua meccanica, ma sono ancora confuso su quando lo userò.

Come qualcuno che di recente ha davvero ottenuto il modello di decoratore e ora sta vedendo gli usi per esso assolutamente ovunque mi piacerebbe essere in grado di capire davvero intuitivamente questo modello apparentemente utile pure.

Non ho molta familiarità con il pattern Visitor. Vediamo se ho capito bene. Supponiamo che tu abbia una gerarchia di animali

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

(Supponiamo che sia una gerarchia complessa con un’interfaccia ben stabilita.)

Ora vogliamo aggiungere una nuova operazione alla gerarchia, ovvero vogliamo che ogni animale faccia il suo suono. Per quanto la gerarchia sia così semplice, puoi farlo con il polimorfismo lineare:

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

Ma procedendo in questo modo, ogni volta che si desidera aggiungere un'operazione, è necessario modificare l'interfaccia per ogni singola class della gerarchia. Ora, supponiamo invece di essere soddisfatto dell'interfaccia originale e di voler apportare le modifiche il meno ansible.

Il pattern Visitor consente di spostare ogni nuova operazione in una class appropriata, ed è necessario estendere l'interfaccia della gerarchia solo una volta. Facciamolo. Per prima cosa, definiamo un'operazione astratta (la class "Visitor" in GoF) che ha un metodo per ogni class nella gerarchia:

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

Quindi, modifichiamo la gerarchia per accettare nuove operazioni:

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

Infine, implementiamo l'operazione effettiva, senza modificare né Cat né Dog :

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

Ora hai un modo per aggiungere operazioni senza modificare la gerarchia. Ecco come funziona:

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

Il motivo della tua confusione è probabilmente che il visitatore è un termine improprio fatale. Molti programmatori (prominenti!) Si sono imbattuti in questo problema. Ciò che effettivamente fa è implementare il doppio dispacciamento in lingue che non lo supportano in modo nativo (molti di loro non lo fanno).


1) Il mio esempio preferito è Scott Meyers, acclamato autore di “Effective C ++”, che ha definito questo uno dei suoi più importanti a ++ del C ++! momentjs di sempre

Tutti qui sono corretti, ma penso che non riesca ad affrontare il “quando”. Innanzitutto, dai modelli di progettazione:

Visitor consente di definire una nuova operazione senza modificare le classi degli elementi su cui opera.

Ora, pensiamo ad una semplice gerarchia di classi. Ho classi 1, 2, 3 e 4 e metodi A, B, C e D. Disporle come in un foglio di calcolo: le classi sono linee ei metodi sono colonne.

Ora, il design orientato agli oggetti presume che sia più probabile che crei nuove classi rispetto ai nuovi metodi, quindi aggiungere più linee, per così dire, è più semplice. Devi solo aggiungere una nuova class, specificare cosa c’è di diverso in quella class ed ereditare il resto.

A volte, tuttavia, le classi sono relativamente statiche, ma è necessario aggiungere più metodi frequentemente, aggiungendo colonne. Il modo standard in un design OO sarebbe quello di aggiungere tali metodi a tutte le classi, il che può essere costoso. Il modello Visitor lo rende facile.

A proposito, questo è il problema che il modello di Scala coincide con l’intento di risolvere.

Il pattern di progettazione Visitor funziona molto bene per strutture “ricorsive” come alberi di directory, strutture XML o profili di documenti.

Un object Visitor visita ogni nodo nella struttura ricorsiva: ogni directory, ogni tag XML, qualunque sia. L’object Visitor non esegue il ciclo attraverso la struttura. Invece i metodi Visitor sono applicati a ciascun nodo della struttura.

Ecco una tipica struttura ricorsiva dei nodes. Potrebbe essere una directory o un tag XML. [Se sei una persona Java, immagina un sacco di metodi extra per build e mantenere l’elenco dei bambini.]

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

Il metodo di visit applica un object Visitor a ciascun nodo nella struttura. In questo caso, è un visitatore top-down. È ansible modificare la struttura del metodo di visit per eseguire un comando bottom-up o altri ordini.

Ecco una superclass per i visitatori. È usato dal metodo di visit . “Arriva a” ogni nodo nella struttura. Poiché il metodo di visit chiama up e down , il visitatore può tenere traccia della profondità.

 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 

Una sottoclass può fare cose come i nodes di conteggio ad ogni livello e accumulare un elenco di nodes, generando un buon numero di sezioni gerarchiche del percorso.

Ecco una domanda. Costruisce una struttura ad albero, someTree albero. Crea un Visitor , dumpNodes .

Quindi applica i dumpNodes all’albero. L’object dumpNode “visiterà” ciascun nodo nell’albero.

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

L’algoritmo di visit TreeNode assicurerà che ogni TreeNode è utilizzato come argomento per il metodo arrivedAt del visitatore.

Un modo per vederlo è che il pattern visitor è un modo per consentire ai client di aggiungere metodi aggiuntivi a tutte le classi in una particolare gerarchia di classi.

È utile quando si ha una gerarchia di classi abbastanza stabile, ma si cambiano i requisiti di ciò che deve essere fatto con quella gerarchia.

L’esempio classico è per compilatori e simili. Un Abstract Syntax Tree (AST) può definire con precisione la struttura del linguaggio di programmazione, ma le operazioni che potresti voler fare su AST cambieranno con l’avanzare del tuo progetto: generatori di codice, pretty-printer, debugger, analisi delle metriche di complessità.

Senza il pattern Visitor, ogni volta che uno sviluppatore voleva aggiungere una nuova funzionalità, avrebbe bisogno di aggiungere quel metodo a ogni funzione della class base. Questo è particolarmente difficile quando le classi di base appaiono in una libreria separata o sono prodotte da un team separato.

(Ho sentito dire che il pattern Visitor è in conflitto con buone pratiche OO, perché sposta le operazioni dei dati lontano dai dati. Il pattern Visitor è utile proprio nella situazione in cui le normali pratiche OO falliscono).

Ci sono almeno tre ottimi motivi per utilizzare il pattern Visitor:

  1. Ridurre la proliferazione di codice che è solo leggermente diversa quando cambiano le strutture dei dati.

  2. Applicare lo stesso calcolo a diverse strutture di dati, senza modificare il codice che implementa il calcolo.

  3. Aggiungi informazioni alle librerie legacy senza modificare il codice legacy.

Si prega di dare un’occhiata a un articolo che ho scritto su questo .

Come già sottolineato da Konrad Rudolph, è adatto per i casi in cui è necessaria una doppia spedizione

Ecco un esempio per mostrare una situazione in cui è necessario un doppio invio e in che modo il visitatore ci aiuta a farlo.

Esempio :

Diciamo che ho 3 tipi di dispositivi mobili: iPhone, Android, Windows Mobile.

Tutti e tre questi dispositivi hanno una radio Bluetooth installata in essi.

Supponiamo che la radio bluetooth possa provenire da 2 OEM separati: Intel e Broadcom.

Giusto per rendere l’esempio rilevante per la nostra discussione, lascia anche supporre che le API esposte dalla radio Intel siano diverse da quelle esposte dalla radio Broadcom.

Ecco come appaiono le mie lezioni –

inserisci la descrizione dell'immagine qui inserisci la descrizione dell'immagine qui

Ora, vorrei introdurre un’operazione – Accendere il Bluetooth sul dispositivo mobile.

La sua firma di funzione dovrebbe piacere qualcosa di simile –

  void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio) 

Quindi, a seconda del tipo di dispositivo giusto e in base al tipo giusto di radio Bluetooth , può essere acceso chiamando i passaggi appropriati o l’algoritmo .

In linea di principio, diventa una matrice 3 x 2, in cui sto cercando di convogliare l’operazione giusta in base al tipo giusto di oggetti coinvolti.

Un comportamento polimorfico che dipende dal tipo di entrambi gli argomenti.

inserisci la descrizione dell'immagine qui

Ora, il pattern Visitor può essere applicato a questo problema. L’ispirazione viene dalla pagina di Wikipedia che afferma: “In sostanza, il visitatore consente di aggiungere nuove funzioni virtuali a una famiglia di classi senza modificare le classi stesse; al contrario, si crea una class di visitatori che implementa tutte le specializzazioni appropriate della funzione virtuale. Il visitatore prende il riferimento di istanza come input e implementa l’objective attraverso il doppio invio. “

La doppia spedizione è necessaria qui grazie alla matrice 3×2

Ecco come apparirà il set-up: inserisci la descrizione dell'immagine qui

Ho scritto l’esempio per rispondere ad un’altra domanda, il codice e la sua spiegazione sono menzionati qui .

Ho trovato più facile nei seguenti link:

In http://www.remondo.net/visitor-pattern-example-csharp/ ho trovato un esempio che mostra un esempio di simulazione che mostra quali sono i vantaggi del modello di visitatore. Qui hai diverse classi di contenitori per 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; } } } 

Come vedi sopra, You BilsterPack contiene coppie di pillole ‘quindi devi moltiplicare il numero di coppie di 2. Inoltre potresti notare che Bottle usa unit che è un tipo di dati differente e deve essere lanciato.

Quindi nel metodo principale puoi calcolare il conteggio delle pillole usando il seguente codice:

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

Si noti che il codice di cui sopra viola il Single Responsibility Principle . Ciò significa che devi modificare il codice del metodo principale se aggiungi un nuovo tipo di contenitore. Anche fare più passaggi è una ctriggers pratica.

Quindi, introducendo il seguente codice:

 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 } 

Hai trasferito la responsabilità del conteggio del numero di Pill alla class denominata PillCountVisitor (e abbiamo rimosso la dichiarazione del caso switch). Questo significa che ogni volta che devi aggiungere un nuovo tipo di contenitore per pillole devi modificare solo la class PillCountVisitor . Notare IVisitor interfaccia IVisitor è generale per l’utilizzo in altri scenari.

Aggiungendo il metodo Accept alla pill package class:

 public class BlisterPack : IAcceptor { public int TabletPairs { get; set; } #region IAcceptor Members public void Accept(IVisitor visitor) { visitor.Visit(this); } #endregion } 

permettiamo al visitatore di visitare le classi di contenitori di pillole.

Alla fine calcoliamo il conteggio delle pillole usando il seguente codice:

 var visitor = new PillCountVisitor(); foreach (IAcceptor item in packageList) { item.Accept(visitor); } 

Ciò significa che: ogni contenitore di pillole consente al visitatore di PillCountVisitor di vedere le loro pillole contare. Lui sa come contare la pillola.

Al visitor.Count ha il valore delle pillole.

In http://butunclebob.com/ArticleS.UncleBob.IuseVisitor viene visualizzato uno scenario reale in cui non è ansible utilizzare il polimorfismo (la risposta) per seguire il principio di responsabilità singola. Infatti in:

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

il metodo reportQtdHoursAndPay è per la segnalazione e la rappresentazione e questo viola il Principio di Responsabilità Unica. Quindi è meglio usare il pattern visitatore per superare il problema.

A mio parere, la quantità di lavoro per aggiungere una nuova operazione è più o meno la stessa usando Visitor Pattern o la modifica diretta di ogni struttura di elementi. Inoltre, se dovessi aggiungere una nuova class di elementi, ad esempio Cow , l’interfaccia Operation verrà interessata e si propaga a tutte le classi di elementi esistenti, richiedendo quindi la ricompilazione di tutte le classi di elementi. Allora, qual è il punto?

Visitor Pattern come la stessa implementazione underground per Aspect Object programming.

Ad esempio se si definisce una nuova operazione senza modificare le classi degli elementi su cui opera

Cay Horstmann ha un ottimo esempio di dove applicare Visitor nel suo libro OO Design e modelli . Riassume il problema:

Gli oggetti composti hanno spesso una struttura complessa, composta da singoli elementi. Alcuni elementi possono avere di nuovo elementi figlio. … Un’operazione su un elemento visita i suoi elementi figli, applica loro l’operazione e combina i risultati. … Tuttavia, non è facile aggiungere nuove operazioni a tale progetto.

Il motivo per cui non è facile è perché le operazioni vengono aggiunte all’interno delle stesse classi di struttura. Ad esempio, immagina di avere un File System:

Diagramma di classe FileSystem

Ecco alcune operazioni (funzionalità) che potremmo voler implementare con questa struttura:

  • Mostra i nomi degli elementi del nodo (un elenco di file)
  • Mostra la dimensione calcasting degli elementi del nodo (dove la dimensione di una directory include la dimensione di tutti i suoi elementi figli)
  • eccetera.

È ansible aggiungere funzioni a ogni class nel FileSystem per implementare le operazioni (e le persone lo hanno fatto in passato in quanto è molto ovvio come farlo). Il problema è che ogni volta che aggiungi una nuova funzionalità (la riga “ecc.” Sopra), potresti dover aggiungere sempre più metodi alle classi della struttura. Ad un certo punto, dopo un certo numero di operazioni che hai aggiunto al tuo software, i metodi in quelle classi non hanno più senso in termini di coesione funzionale delle classi. Ad esempio, si dispone di un FileNode che ha un metodo calculateFileColorForFunctionABC() per implementare la funzionalità di visualizzazione più recente sul file system.

The Visitor Pattern (come molti modelli di design) è nato dal dolore e dalla sofferenza degli sviluppatori che sapevano che c’era un modo migliore per consentire al loro codice di cambiare senza richiedere molti cambiamenti ovunque e nel rispetto di buoni principi di progettazione (alta coesione, basso accoppiamento ). È mia opinione che sia difficile capire l’utilità di molti modelli finché non hai sentito quel dolore. Spiegare il dolore (come cerchiamo di fare sopra con le funzionalità “ecc.” Che vengono aggiunte) occupa spazio nella spiegazione ed è una distrazione. Comprendere i modelli è difficile per questo motivo.

Il visitatore ci consente di disaccoppiare le funzionalità sulla struttura dei dati (ad es. FileSystemNodes ) dalle strutture di dati stesse. Il modello consente al design di rispettare la coesione – le classi della struttura dei dati sono più semplici (hanno meno metodi) e anche le funzionalità sono incapsulate nelle implementazioni di Visitor . Questo viene fatto tramite il doppio dispacciamento (che è la parte complicata del modello): usando i metodi accept() nelle classi structure e visitX() metodi visitX() nelle classi Visitor (the function):

Schema di classe FileSystem con Visitor applicato

Questa struttura ci consente di aggiungere nuove funzionalità che funzionano sulla struttura come visitatori concreti (senza modificare le classi di struttura).

Schema di classe FileSystem con Visitor applicato

Ad esempio, un PrintNameVisitor che implementa la funzionalità di elenco di directory e un PrintSizeVisitor che implementa la versione con la dimensione. Potremmo immaginare un giorno avere un ‘ExportXMLVisitor` che genera i dati in XML, o un altro visitatore che lo genera in JSON, ecc. Potremmo anche avere un visitatore che visualizza la mia struttura di directory usando un linguaggio grafico come DOT , per essere visualizzato con un altro programma.

Come nota finale: la complessità di Visitor con la sua doppia spedizione significa che è più difficile capire, codificare e fare il debug. In breve, ha un alto fattore geek e ribadisce il principio KISS. In un sondaggio fatto dai ricercatori, il visitatore ha dimostrato di essere un modello controverso (non c’era un consenso sulla sua utilità). Alcuni esperimenti hanno persino dimostrato che non era più facile mantenere il codice.

Breve descrizione del modello di visitatore. Le classi che richiedono modifiche devono tutte implementare il metodo “accetta”. I client chiamano questo metodo accept per eseguire qualche nuova azione su quella famiglia di classi estendendo così le loro funzionalità. I client sono in grado di utilizzare questo metodo di accettazione per eseguire una vasta gamma di nuove azioni passando in una class di visitatori diversa per ogni azione specifica. Una class visitatore contiene più metodi di visita sovrascritti che definiscono come ottenere la stessa azione specifica per ogni class all’interno della famiglia. Questi metodi di visita ricevono un’istanza su cui lavorare.

Quando potresti considerare di usarlo

  1. Quando hai una famiglia di classi sai che dovrai aggiungere molte nuove azioni tutte, ma per qualche motivo non sarai in grado di modificare o ricompilare la famiglia di classi in futuro.
  2. Quando si desidera aggiungere una nuova azione e avere quella nuova azione interamente definita all’interno di una class di visitatori anziché distribuirsi su più classi.
  3. Quando il tuo capo dice che devi produrre una serie di classi che devono fare qualcosa al momento ! … ma nessuno in realtà sa esattamente cosa sia ancora quel qualcosa.

Basato sull’ottima risposta di @Federico A. Ramponi.

Immagina di avere questa gerarchia:

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

Cosa succede se devi aggiungere un metodo “Walk” qui? Ciò sarà doloroso per l’intero design.

Allo stesso tempo, l’aggiunta del metodo “Walk” genera nuove domande. Che dire di “Eat” o “Sleep”? Dobbiamo davvero aggiungere un nuovo metodo alla gerarchia Animal per ogni nuova azione o operazione che vogliamo aggiungere? È brutto e molto importante, non saremo mai in grado di chiudere l’interfaccia Animal. Quindi, con il pattern visitor, possiamo aggiungere un nuovo metodo alla gerarchia senza modificare la gerarchia!

Quindi, basta controllare ed eseguire questo esempio C #:

 using System; using System.Collections.Generic; namespace VisitorPattern { class Program { static void Main(string[] args) { var animals = new List { 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); } } } 

Visitatore

Il visitatore consente di aggiungere nuove funzioni virtuali a una famiglia di classi senza modificare le classi stesse; al contrario, si crea una class di visitatori che implementa tutte le specializzazioni appropriate della funzione virtuale

Struttura del visitatore:

inserisci la descrizione dell'immagine qui

Utilizza il pattern Visitor se:

  1. Operazioni simili devono essere eseguite su oggetti di diversi tipi raggruppati in una struttura
  2. È necessario eseguire molte operazioni distinte e non correlate. Separa l’operazione dagli oggetti Struttura
  3. Nuove operazioni devono essere aggiunte senza modifiche nella struttura dell’object
  4. Raccogli le operazioni correlate in una singola class anziché costringerti a modificare o ricavare classi
  5. Aggiungi funzioni alle librerie di classi per le quali non hai la sorgente o non puoi cambiare la fonte

Anche se il pattern Visitor fornisce la flessibilità per aggiungere nuove operazioni senza modificare il codice esistente in Object, questa flessibilità ha un difetto.

Se è stato aggiunto un nuovo object Visitable, richiede modifiche al codice nelle classi Visitor e ConcreteVisitor . Esiste una soluzione alternativa per risolvere questo problema: Utilizzare la riflessione, che avrà un impatto sulle prestazioni.

Snippet di codice:

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

Spiegazione:

  1. Visitable ( Element ) è un’interfaccia e questo metodo di interfaccia deve essere aggiunto a un insieme di classi.
  2. Visitor è un’interfaccia, che contiene metodi per eseguire un’operazione su elementi Visitable .
  3. GameVisitor è una class che implementa l’interfaccia Visitor ( ConcreteVisitor ).
  4. Ogni elemento Visitable accetta Visitor e invoca un metodo rilevante di interfaccia Visitor .
  5. Puoi trattare Game as Element e giochi di cemento come Chess,Checkers and Ludo come ConcreteElements .

Nell’esempio sopra, Chess, Checkers and Ludo sono tre giochi diversi (e classi Visitable ). In un bel giorno, ho incontrato uno scenario per registrare le statistiche di ogni gioco. Quindi, senza modificare la class individuale per implementare la funzionalità statistica, è ansible centralizzare tale responsabilità nella class GameVisitor , che fa il trucco per te senza modificare la struttura di ogni gioco.

produzione:

 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 

Fare riferimento a

oodesign articolo

articolo di sourcemaking

per ulteriori dettagli

Decoratore

pattern consente di aggiungere un comportamento a un singolo object, staticamente o dynamicmente, senza influire sul comportamento di altri oggetti della stessa class

Articoli correlati:

Decorator Pattern for IO

Quando utilizzare il pattern Decorator?

Double dispatch is just one reason among others to use this pattern .
But note that it is the single way to implement double or more dispatch in languages that uses a single dispatch paradigm.

Here are reasons to use the pattern :

1) We want to define new operations without changing the model at each time because the model doesn’t change often wile operations change frequently.

2) We don’t want to couple model and behavior because we want to have a reusable model in multiple applications or we want to have an extensible model that allow client classs to define their behaviors with their own classs.

3) We have common operations that depend on the concrete type of the model but we don’t want to implement the logic in each subclass as that would explode common logic in multiple classs and so in multiple places .

4) We are using a domain model design and model classs of the same hierarchy perform too many distinct things that could be gathered somewhere else .

5) We need a double dispatch .
We have variables declared with interface types and we want to be able to process them according their runtime type … of course without using if (myObj instanceof Foo) {} or any trick.
The idea is for example to pass these variables to methods that declares a concrete type of the interface as parameter to apply a specific processing. This way of doing is not possible out of the box with languages relies on a single-dispatch because the chosen invoked at runtime depends only on the runtime type of the receiver.
Note that in Java, the method (signature) to call is chosen at compile time and it depends on the declared type of the parameters, not their runtime type.

The last point that is a reason to use the visitor is also a consequence because as you implement the visitor (of course for languages that doesn’t support multiple dispatch), you necessarily need to introduce a double dispatch implementation.

Note that the traversal of elements (iteration) to apply the visitor on each one is not a reason to use the pattern.
You use the pattern because you split model and processing.
And by using the pattern, you benefit in addition from an iterator ability.
This ability is very powerful and goes beyond iteration on common type with a specific method as accept() is a generic method.
It is a special use case. So I will put that to one side.


Example in Java

I will illustrate the added value of the pattern with a chess example where we would like to define processing as player requests a piece moving.

Without the visitor pattern use, we could define piece moving behaviors directly in the pieces subclasss.
We could have for example a Piece interface such as :

 public interface Piece{ boolean checkMoveValidity(Coordinates coord); void performMove(Coordinates coord); Piece computeIfKingCheck(); } 

Each Piece subclass would implement it such as :

 public class Pawn implements Piece{ @Override public boolean checkMoveValidity(Coordinates coord) { ... } @Override public void performMove(Coordinates coord) { ... } @Override public Piece computeIfKingCheck() { ... } } 

And the same thing for all Piece subclasss.
Here is a diagram class that illustrates this design :

[model class diagram

This approach presents three important drawbacks :

– behaviors such as performMove() or computeIfKingCheck() will very probably use common logic.
For example whatever the concrete Piece , performMove() will finally set the current piece to a specific location and potentially takes the opponent piece.
Splitting related behaviors in multiple classs instead of gathering them defeats in a some way the single responsibility pattern. Making their maintainability harder.

– processing as checkMoveValidity() should not be something that the Piece subclasss may see or change.
It is check that goes beyond human or computer actions. This check is performsd at each action requested by a player to ensure that the requested piece move is valid.
So we even don’t want to provide that in the Piece interface.

– In chess games challenging for bot developers, generally the application provides a standard API ( Piece interfaces, subclasss, Board, common behaviors, etc…) and let developers enrich their bot strategy.
To be able to do that, we have to propose a model where data and behaviors are not tightly coupled in the Piece implementations.

So let’s go to use the visitor pattern !

We have two kinds of structure :

– the model classs that accept to be visited (the pieces)

– the visitors that visit them (moving operations)

Here is a class diagram that illustrates the pattern :

inserisci la descrizione dell'immagine qui

In the upper part we have the visitors and in the lower part we have the model classs.

Here is the PieceMovingVisitor interface (behavior specified for each kind of 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); } 

The Piece is defined now :

 public interface Piece { void accept(PieceMovingVisitor pieceVisitor); Coordinates getCoordinates(); void setCoordinates(Coordinates coordinates); } 

Its key method is :

 void accept(PieceMovingVisitor pieceVisitor); 

It provides the first dispatch : a invocation based on the Piece receiver.
At compile time, the method is bound to the accept() method of the Piece interface and at runtime, the bounded method will be invoked on the runtime Piece class.
And it is the accept() method implementation that will perform a second dispatch.

Indeed, each Piece subclass that wants to be visited by a PieceMovingVisitor object invokes the PieceMovingVisitor.visit() method by passing as argument itself.
In this way, the compiler bounds as soon as the compile time, the type of the declared parameter with the concrete type.
There is the second dispatch.
Here is the Bishop subclass that illustrates that :

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

And here an usage example :

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

Visitor drawbacks

The Visitor pattern is a very powerful pattern but it also has some important limitations that you should consider before using it.

1) Risk to reduce/break the encapsulation

In some kinds of operation, the visitor pattern may reduce or break the encapsulation of domain objects.

For example, as the MovePerformingVisitor class needs to set the coordinates of the actual piece, the Piece interface has to provide a way to do that :

 void setCoordinates(Coordinates coordinates); 

The responsibility of Piece coordinates changes is now open to other classs than Piece subclasss.
Moving the processing performsd by the visitor in the Piece subclasss is not an option either.
It will indeed create another issue as the Piece.accept() accepts any visitor implementation. It doesn’t know what the visitor performs and so no idea about whether and how to change the Piece state.
A way to identify the visitor would be to perform a post processing in Piece.accept() according to the visitor implementation. It would be a very bad idea as it would create a high coupling between Visitor implementations and Piece subclasss and besides it would probably require to use trick as getClass() , instanceof or any marker identifying the Visitor implementation.

2) Requirement to change the model

Contrary to some other behavioral design patterns as Decorator for example, the visitor pattern is intrusive.
We indeed need to modify the initial receiver class to provide an accept() method to accept to be visited.
We didn’t have any issue for Piece and its subclasss as these are our classs .
In built-in or third party classs, things are not so easy.
We need to wrap or inherit (if we can) them to add the accept() method.

3) Indirections

The pattern creates multiples indirections.
The double dispatch means two invocations instead of a single one :

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

And we could have additional indirections as the visitor changes the visited object state.
It may look like a cycle :

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

While I have understood the how and when, I have never understood the why. In case it helps anyone with a background in a language like C++, you want to read this very carefully.

For the lazy, we use the visitor pattern because “while virtual functions are dispatched dynamically in C++, function overloading is done statically” .

Or, put another way, to make sure that CollideWith(ApolloSpacecraft&) is called when you pass in a SpaceShip reference that is actually bound to an ApolloSpacecraft object.

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

I really like the description and the example from http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html .

The assumption is that you have a primary class hierarchy that is fixed; perhaps it’s from another vendor and you can’t make changes to that hierarchy. However, your intent is that you’d like to add new polymorphic methods to that hierarchy, which means that normally you’d have to add something to the base class interface. So the dilemma is that you need to add methods to the base class, but you can’t touch the base class. How do you get around this?

The design pattern that solves this kind of problem is called a “visitor” (the final one in the Design Patterns book), and it builds on the double dispatching scheme shown in the last section.

The visitor pattern allows you to extend the interface of the primary type by creating a separate class hierarchy of type Visitor to virtualize the operations performsd upon the primary type. The objects of the primary type simply “accept” the visitor, then call the visitor’s dynamically-bound member function.

When you want to have function objects on union data types, you will need visitor pattern.

You might wonder what function objects and union data types are, then it’s worth reading http://www.ccs.neu.edu/home/matthias/htdc.html

Thanks for the awesome explanation of @Federico A. Ramponi , I just made this in java version. Hope it might be helpful.

Also just as @Konrad Rudolph pointed out, it’s actually a double dispatch using two concrete instances together to determine the run-time methods.

So actually there is no need to create a common interface for the operation executor as long as we have the operation interface properly defined.

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

As you expect, a common interface will bring us more clarity though it’s actually not the essential part in this pattern.

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

your question is when to know:

i do not first code with visitor pattern. i code standard and wait for the need to occur & then refactor. so lets say you have multiple payment systems that you installed one at a time. At checkout time you could have many if conditions (or instanceOf) , for example :

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

now imagine i had 10 payment methods, it gets kind of ugly. So when you see that kind of pattern occuring visitor comes in handly to seperate all that out and you end up calling something like this afterwards:

 new PaymentCheckoutVistor(paymentType).visit() 

You can see how to implement it from the number of examples here, im just showing you a usecase.