Quando dovrei usare il modello di progettazione visitatori? [chiuso]


315

Continuo a vedere riferimenti ai modelli di visitatori nei blog ma devo ammettere che non capisco. Ho letto l' articolo di Wikipedia per il modello e capisco la sua meccanica, ma sono ancora confuso su quando lo userei.

Come qualcuno che di recente ha davvero ottenuto il motivo decorativo e ora sta vedendo gli usi assolutamente ovunque, mi piacerebbe essere in grado di comprendere davvero intuitivamente anche questo motivo apparentemente utile.


7
Alla fine l'ho capito dopo aver letto questo articolo di Jermey Miller sul mio blackberry mentre sono rimasto in attesa in una hall per due ore. È lungo ma offre una meravigliosa spiegazione di doppio invio, visitatore e composito, e cosa puoi fare con questi.
George Mauer,


3
Modello visitatore? Quale? Il punto è: c'è un sacco di incomprensioni e pura confusione attorno a questo modello di design. Ho scritto e pubblicato un articolo che spera dia un
Richard Gomes

Quando si desidera disporre di oggetti funzione su tipi di dati unione, è necessario il modello visitatore. Potresti chiederti quali sono gli oggetti funzione e i tipi di dati di unione, quindi vale la pena leggere ccs.neu.edu/home/matthias/htdc.html
Wei Qiu

Esempi qui e qui .
jaco0646,

Risposte:


315

Non ho molta familiarità con il modello Visitatore. Vediamo se ho capito bene. Supponiamo di avere una gerarchia di animali

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

(Supponiamo che sia una gerarchia complessa con un'interfaccia ben consolidata.)

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 un polimorfismo diretto:

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

Procedendo in questo modo, ogni volta che si desidera aggiungere un'operazione è necessario modificare l'interfaccia in ogni singola classe della gerarchia. Supponiamo ora di essere soddisfatto dell'interfaccia originale e di voler apportare il minor numero possibile di modifiche.

Il modello Visitatore consente di spostare ogni nuova operazione in una classe adatta ed è necessario estendere l'interfaccia della gerarchia solo una volta. Facciamolo. Innanzitutto, definiamo un'operazione astratta (la classe "Visitor" in GoF ) che ha un metodo per ogni classe 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 reale, 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 più la gerarchia. Ecco come funziona:

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

19
S.Lott, camminare su un albero non è in realtà il modello del visitatore. (È il "modello gerarchico di visitatori", che è completamente diverso in modo confuso.) Non c'è modo di mostrare il modello di visitatori GoF senza utilizzare l'ereditarietà o l'implementazione dell'interfaccia.
munifico

14
@Knownasilya - Non è vero. L'& -Operator fornisce l'indirizzo del Sound-Object, necessario all'interfaccia. letsDo(Operation *v) ha bisogno di un puntatore.
AquilaRapax,

3
solo per chiarezza, questo esempio di modello di progettazione dei visitatori è corretto?
Godzilla,

4
Dopo aver riflettuto molto, mi chiedo perché hai chiamato due metodi hereIsADog e hereIsACat sebbene tu abbia già passato Dog e Cat ai metodi. Preferirei un semplice performTask (Object * obj) e tu lanci questo oggetto nella classe Operation. (e nel linguaggio che supporta l'override, non è necessario il casting)
Abdalrahman Shatou,

6
Nel tuo esempio "principale" alla fine: theSound.hereIsACat(c)avresti fatto il lavoro, come giustifichi tutto il sovraccarico introdotto dal modello? il doppio dispacciamento è la giustificazione.
franssu,

131

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


1) Il mio esempio preferito è Scott Meyers, acclamato autore di "C ++ efficace", che ha definito questo uno dei suoi più importanti C ++ aha! momenti di sempre .


3
+1 "non c'è schema" - la risposta perfetta. la risposta più votata dimostra che molti programmatori c ++ devono ancora realizzare i limiti delle funzioni virtuali sul polimorfismo "ad hoc" usando un tipo enum e switch case (il modo c). Potrebbe essere più ordinato e invisibile utilizzare il virtuale, ma è ancora limitato al singolo invio. Secondo la mia opinione personale, questo è il più grande difetto del c ++.
user3125280,

@ user3125280 Ho letto 4/5 articoli e il capitolo Modelli di disegno sul modello Visitatore ora, e nessuno di essi spiega il vantaggio di usare questo modello oscuro su un caso stmt, o quando potresti usarne uno sull'altro. Grazie per almeno averlo sollevato!
Spinkus,

4
@ Sam Sono abbastanza sicuro che spiegano - è lo stesso vantaggio che si sempre ottiene dal polimorfismo sottoclasse / runtime sopra switch: switchhard-codifica il processo decisionale a lato client (duplicazione del codice) e non offre controllo di tipo statico ( verificare la completezza e la distinzione dei casi ecc.). Un modello di visitatore viene verificato dal controllo del tipo e di solito semplifica il codice client.
Konrad Rudolph,

@KonradRudolph grazie per quello. Tuttavia, non viene affrontato esplicitamente in Patterns o nell'articolo di Wikipedia, ad esempio. Non sono in disaccordo con te, ma potresti argomentare che ci sono dei vantaggi nell'usare un caso stmt, quindi è strano che non sia generalmente contrastato: 1. Non hai bisogno di un metodo accept () su oggetti della tua collezione. 2. Il visitatore ~ ​​può gestire oggetti di tipo sconosciuto. Pertanto, il caso stmt sembra adattarsi meglio per operare su strutture di oggetti con una raccolta variabile di tipi coinvolti. Patterns ammette che il pattern Visitor non è adatto a tale scenario (p333).
spinkus,

1
@SamPinkus konrad: ecco perché le virtualcaratteristiche simili sono così utili nei moderni linguaggi di programmazione - sono i mattoni di base dei programmi estensibili - secondo me il modo c (interruttore nidificato o corrispondenza di pattern, ecc. A seconda della lingua scelta) è molto più pulito nel codice che non ha bisogno di essere estensibile e sono stato piacevolmente sorpreso di vedere questo stile in software complicato come il prover 9. Ancora più importante, qualsiasi linguaggio che voglia fornire estensibilità dovrebbe probabilmente ospitare modelli di invio migliori rispetto al singolo invio ricorsivo (ad es. visitatore).
user3125280,

84

Tutti qui hanno ragione, ma penso che non riesca ad affrontare il "quando". Innanzitutto, da Design Patterns:

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

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

Ora, il design orientato agli oggetti presume che sia più probabile che cresca nuove classi rispetto a nuovi metodi, quindi aggiungere più linee, per così dire, è più facile. Devi solo aggiungere una nuova classe, specificare ciò che è diverso in quella classe ed ereditare il resto.

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

A proposito, questo è il problema che le combinazioni di schemi di Scala intende risolvere.


Perché dovrei usare il modello visitatore solo su una classe di utilità? posso chiamare la mia classe di utilità in questo modo: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Qual è la differenza ? entrambi fanno la separazione delle preoccupazioni giusto? spero che tu possa aiutare.
j2emanue,

@ j2emanue Perché il modello di visitatore utilizza il sovraccarico di visitatore corretto in fase di esecuzione. Mentre il tuo codice ha bisogno di cast di tipo per chiamare il sovraccarico corretto.
Accesso negato il

c'è un guadagno di efficienza con quello? Immagino che eviti di lanciare è una buona idea
j2emanue,

@ j2emanue l'idea è quella di scrivere un codice conforme al principio aperto / chiuso, non motivi di prestazione. Vedi aperto chiuso a zio Bob butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Accesso negato il

22

Il modello di progettazione del visitatore funziona davvero bene per strutture "ricorsive" come alberi di directory, strutture XML o contorni di documenti.

Un oggetto Visitor visita ogni nodo nella struttura ricorsiva: ogni directory, ogni tag XML, qualunque cosa. L'oggetto Visitor non scorre attraverso la struttura. Invece i metodi Visitor vengono applicati a ciascun nodo della struttura.

Ecco una tipica struttura di nodo ricorsivo. Potrebbe essere una directory o un tag XML. [Se sei una persona Java, immagina molti metodi extra per costruire 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 visitmetodo applica un oggetto Visitor a ciascun nodo nella struttura. In questo caso, è un visitatore dall'alto verso il basso. È possibile modificare la struttura del visitmetodo per eseguire il bottom-up o altri ordini.

Ecco una superclasse per i visitatori. È usato dal visitmetodo. "Arriva a" ogni nodo nella struttura. Poiché il visitmetodo chiama upe 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 sottoclasse potrebbe fare cose come contare i nodi ad ogni livello e accumulare un elenco di nodi, generando un bel numero di sezioni gerarchiche di percorso.

Ecco un'applicazione. Si costruisce una struttura ad albero, someTree. Esso crea un Visitor, dumpNodes.

Quindi applica dumpNodesl'albero all'albero. L' dumpNodeoggetto "visiterà" ciascun nodo nella struttura.

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

L' visitalgoritmo TreeNode assicurerà che ogni TreeNode sia usato come argomento per il arrivedAtmetodo del Visitatore .


8
Come altri hanno affermato, questo è il "modello gerarchico di visitatori".
PPC-Coder,

1
@ PPC-Coder Qual è la differenza tra "modello gerarchico di visitatori" e modello di visitatori?
Tim Lovell-Smith,

3
Il modello di visitatore gerarchico è più flessibile del modello di visitatore classico. Ad esempio, con il modello gerarchico è possibile tenere traccia della profondità della traversata e decidere quale ramo attraversare o interrompere la traversata tutti insieme. Il visitatore classico non ha questo concetto e visiterà tutti i nodi.
Codificatore PPC

18

Un modo per osservarlo è che il modello visitatore è un modo per consentire ai tuoi clienti di aggiungere metodi aggiuntivi a tutte le classi in una particolare gerarchia di classi.

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

L'esempio classico è per compilatori e simili. Un albero di sintassi astratto (AST) può definire con precisione la struttura del linguaggio di programmazione, ma le operazioni che potresti voler fare sull'AST cambieranno man mano che il tuo progetto avanza: generatori di codice, graziose stampanti, debugger, analisi delle metriche di complessità.

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

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


Vorrei anche la tua opinione su quanto segue: Perché dovrei usare il modello visitatore solo su una classe di utlità. posso chiamare la mia classe di utilità in questo modo: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Qual è la differenza ? entrambi fanno la separazione delle preoccupazioni giusto? spero che tu possa aiutare.
j2emanue,

@ j2emanue: non capisco la domanda. Ti suggerisco di approfondire e pubblicare come una domanda completa a cui tutti possano rispondere.
Pensando in modo strano il

1
Ho inviato una nuova domanda qui: stackoverflow.com/questions/52068876/...
j2emanue

14

Esistono almeno tre ottimi motivi per utilizzare il modello visitatore:

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

  2. Applicare lo stesso calcolo a più strutture 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 .


1
Ho commentato il tuo articolo con il più grande uso che ho visto per i visitatori. Pensieri?
George Mauer,

13

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

Ecco un esempio per mostrare una situazione in cui abbiamo bisogno di 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 i dispositivi hanno una radio Bluetooth installata al loro interno.

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

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

Ecco come appaiono le mie lezioni -

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

Ora vorrei introdurre un'operazione: accendere il Bluetooth sul dispositivo mobile.

La firma della sua funzione dovrebbe essere simile a questa:

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

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

In linea di principio, diventa una matrice 3 x 2, in cui sto cercando di vettorializzare l'operazione giusta a seconda del giusto tipo di oggetti coinvolti.

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

inserisci qui la descrizione dell'immagine

Ora, il modello Visitatore può essere applicato a questo problema. L'ispirazione viene dalla pagina di Wikipedia in cui si afferma: “In sostanza, il visitatore consente di aggiungere nuove funzioni virtuali a una famiglia di classi senza modificare le classi stesse; invece, si crea una classe visitatore che implementa tutte le specializzazioni appropriate della funzione virtuale. Il visitatore prende il riferimento all'istanza come input e implementa l'obiettivo tramite doppio invio. "

La doppia spedizione è una necessità qui a causa della matrice 3x2

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

Ho scritto l'esempio per rispondere a un'altra domanda, il codice e la sua spiegazione sono citati qui .


9

Ho trovato più facile nei seguenti link:

In http://www.remondo.net/visitor-pattern-example-csharp/ ho trovato un esempio che mostra un esempio falso 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, BilsterPackcontiene coppie di Pillole, quindi è necessario moltiplicare il numero di coppie per 2. Inoltre, è possibile notare l' Bottleutilizzo di unitun tipo di dati diverso e che è necessario eseguire il cast.

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 sopra riportato viola Single Responsibility Principle. Ciò significa che è necessario modificare il codice del metodo principale se si aggiunge un nuovo tipo di contenitore. Anche prolungare il passaggio è una cattiva 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 spostato la responsabilità del conteggio del numero di Pills nella classe chiamata PillCountVisitor(E abbiamo rimosso l'istruzione case switch). Ciò significa che ogni volta che è necessario aggiungere un nuovo tipo di contenitore per pillole, è necessario cambiare solo PillCountVisitorclasse. Si noti inoltre che l' IVisitorinterfaccia è generale per l'utilizzo in altri scenari.

Aggiungendo il metodo Accept alla classe contenitore pillola:

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

    #region IAcceptor Members

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

    #endregion
}

consentiamo ai visitatori di visitare le classi di contenitori per 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 PillCountVisitorvisitatore di vedere contare le proprie pillole. Sa contare la tua pillola.

Al visitor.Countha il valore delle pillole.

In http://butunclebob.com/ArticleS.UncleBob.IuseVisitor viene visualizzato uno scenario reale in cui non è possibile 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 reportQtdHoursAndPaymetodo è per la segnalazione e la rappresentazione e ciò viola il principio di responsabilità singola. Quindi è meglio usare il modello visitatore per superare il problema.


2
Ciao, puoi modificare la tua risposta per aggiungere parti che hai trovato più illuminante. SO generalmente scoraggia le risposte di solo collegamento poiché l'obiettivo è quello di essere un database di conoscenza e i collegamenti si interrompono.
George Mauer,

8

La doppia spedizione è solo una delle ragioni per utilizzare questo modello .
Si noti tuttavia che è l'unico modo per implementare il doppio o più invio in linguaggi che utilizzano un unico paradigma di invio.

Ecco alcuni motivi per utilizzare il modello:

1) Vogliamo definire nuove operazioni senza cambiare il modello ogni volta perché il modello non cambia spesso mentre le operazioni cambiano frequentemente.

2) Non vogliamo abbinare modello e comportamento perché vogliamo avere un modello riutilizzabile in più applicazioni o vogliamo avere un modello estensibile che consenta alle classi client di definire i loro comportamenti con le proprie classi.

3) Abbiamo operazioni comuni che dipendono dal tipo concreto del modello ma non vogliamo implementare la logica in ciascuna sottoclasse in quanto esploderà la logica comune in più classi e quindi in più posizioni .

4) Stiamo usando un modello di progettazione del dominio e le classi di modello della stessa gerarchia eseguono troppe cose distinte che potrebbero essere raccolte altrove .

5) Abbiamo bisogno di una doppia spedizione .
Abbiamo variabili dichiarate con tipi di interfaccia e vogliamo essere in grado di elaborarle in base al loro tipo di runtime ... ovviamente senza usare if (myObj instanceof Foo) {}o alcun trucco.
L'idea è, ad esempio, di passare queste variabili a metodi che dichiarano un tipo concreto di interfaccia come parametro per applicare un'elaborazione specifica. Questo modo di fare non è possibile out of the box con le lingue si basa su un singolo invio perché il richiamo scelto in fase di runtime dipende solo dal tipo di runtime del ricevitore.
Si noti che in Java, il metodo (firma) da chiamare viene scelto al momento della compilazione e dipende dal tipo dichiarato di parametri, non dal tipo di runtime.

L'ultimo punto che è un motivo per utilizzare il visitatore è anche una conseguenza perché quando si implementa il visitatore (ovviamente per le lingue che non supportano l'invio multiplo), è necessario introdurre un'implementazione a doppio invio.

Si noti che l'attraversamento di elementi (iterazione) per applicare il visitatore su ognuno di essi non è un motivo per utilizzare il modello.
Si utilizza il modello perché è stato diviso il modello e l'elaborazione.
E usando il modello, beneficerai inoltre di un'abilità iteratore.
Questa capacità è molto potente e va oltre l'iterazione sul tipo comune con un metodo specifico come accept()un metodo generico.
È un caso d'uso speciale. Quindi lo metterò da parte.


Esempio in Java

Illustrerò il valore aggiunto del modello con un esempio di scacchi in cui vorremmo definire l'elaborazione mentre il giocatore richiede un pezzo in movimento.

Senza l'uso del modello visitatore, potremmo definire comportamenti di spostamento dei pezzi direttamente nelle sottoclassi dei pezzi.
Potremmo avere ad esempio Pieceun'interfaccia come:

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

Ogni sottoclasse di Piece lo implementerebbe come:

public class Pawn implements Piece{

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

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

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

}

E la stessa cosa per tutte le sottoclassi di Piece.
Ecco una classe di diagramma che illustra questo disegno:

[diagramma di classe del modello

Questo approccio presenta tre importanti inconvenienti:

- comportamenti come performMove()o computeIfKingCheck()molto probabilmente useranno una logica comune.
Ad esempio, qualunque sia il calcestruzzo Piece,performMove() imposterà finalmente il pezzo corrente in una posizione specifica e potenzialmente prenderà il pezzo avversario.
Dividere i comportamenti correlati in più classi invece di raccoglierli sconfigge in qualche modo il singolo modello di responsabilità. Rendere più difficile la loro manutenibilità.

- l'elaborazione come checkMoveValidity()non dovrebbe essere qualcosa che le Piecesottoclassi possono vedere o cambiare.
È un controllo che va oltre le azioni umane o informatiche. Questo controllo viene eseguito ad ogni azione richiesta da un giocatore per assicurarsi che la mossa richiesta sia valida.
Quindi non vogliamo nemmeno fornirlo Piecenell'interfaccia.

- Nei giochi di scacchi stimolanti per gli sviluppatori di bot, in genere l'applicazione fornisce un'API standard ( Pieceinterfacce, sottoclassi, scheda, comportamenti comuni, ecc ...) e consente agli sviluppatori di arricchire la propria strategia di bot.
Per poterlo fare, dobbiamo proporre un modello in cui dati e comportamenti non siano strettamente accoppiati nelle Pieceimplementazioni.

Quindi andiamo ad usare il modello visitatore!

Abbiamo due tipi di struttura:

- le classi modello che accettano di essere visitate (i pezzi)

- i visitatori che li visitano (operazioni di trasloco)

Ecco un diagramma di classe che illustra il modello:

inserisci qui la descrizione dell'immagine

Nella parte superiore abbiamo i visitatori e nella parte inferiore abbiamo le classi del modello.

Ecco l' PieceMovingVisitorinterfaccia (comportamento specificato per ogni tipo di 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);

}

Il pezzo è ora definito:

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

Il suo metodo chiave è:

void accept(PieceMovingVisitor pieceVisitor);

Fornisce il primo invio: un'invocazione basata sul Piecedestinatario.
In fase di compilazione, il metodo è associato al accept()metodo dell'interfaccia di Piece e in fase di esecuzione, il metodo limitato verrà richiamato sulla Piececlasse di runtime .
Ed è ilaccept() implementazione metodo che eseguirà una seconda spedizione.

In effetti, ogni Piecesottoclasse che vuole essere visitata da un PieceMovingVisitoroggetto invoca il PieceMovingVisitor.visit()metodo passando come argomento stesso.
In questo modo, il compilatore limita non appena il tempo di compilazione, il tipo del parametro dichiarato con il tipo concreto.
C'è la seconda spedizione.
Ecco la Bishopsottoclasse che illustra che:

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

}

E qui un esempio di utilizzo:

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

Svantaggi del visitatore

Il modello Visitatore è un modello molto potente, ma presenta anche alcune importanti limitazioni da considerare prima di utilizzarlo.

1) Rischio di ridurre / rompere l'incapsulamento

In alcuni tipi di operazioni, il modello visitatore può ridurre o interrompere l'incapsulamento di oggetti di dominio.

Ad esempio, poiché la MovePerformingVisitor classe deve impostare le coordinate del pezzo reale, l' Pieceinterfaccia deve fornire un modo per farlo:

void setCoordinates(Coordinates coordinates);

La responsabilità delle Piecemodifiche alle coordinate è ora aperta ad altre classi oltre alle Piecesottoclassi.
Anche lo spostamento dell'elaborazione eseguita dal visitatore nelle Piecesottoclassi non è un'opzione.
Creerà sicuramente un altro problema poiché Piece.accept()accetta qualsiasi implementazione dei visitatori. Non sa cosa esegue il visitatore e quindi non ha idea di se e come cambiare lo stato di Piece.
Un modo per identificare il visitatore sarebbe quello di eseguire una post elaborazione Piece.accept()secondo l'implementazione del visitatore. Sarebbe una pessima idea in quanto creerebbe un elevato accoppiamento tra le implementazioni dei visitatori e le sottoclassi di Piece e inoltre richiederebbe probabilmente un trucco come getClass(),instanceof o qualsiasi indicatore che identifichi l'implementazione dei visitatori.

2) Requisito per modificare il modello

Contrariamente ad altri modelli di progettazione comportamentale come Decoratorad esempio, il modello di visitatore è invadente.
Dobbiamo davvero modificare la classe iniziale del ricevitore per fornire un accept()metodo per accettare di essere visitati.
Non abbiamo avuto alcun problema per Piecee sue sottoclassi in quanto queste sono le nostre classi .
Nelle classi integrate o di terze parti, le cose non sono così facili.
Abbiamo bisogno di avvolgerli o ereditarli (se possibile) per aggiungere il accept()metodo.

3) Indiretti

Il modello crea più riferimenti indiretti.
La doppia spedizione significa due invocazioni anziché una sola:

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

E potremmo avere ulteriori indicazioni indirette quando il visitatore cambia lo stato dell'oggetto visitato.
Può sembrare un ciclo:

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

6

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

Gli oggetti composti hanno spesso una struttura complessa, composta da singoli elementi. Alcuni elementi potrebbero nuovamente avere elementi figlio. ... Un'operazione su un elemento visita i suoi elementi figlio, applica l'operazione a loro 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 strutture. Ad esempio, immagina di avere un file system:

Diagramma di classe del file system

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

  • Visualizza i nomi degli elementi del nodo (un elenco di file)
  • Visualizza la dimensione calcolata degli elementi del nodo (dove la dimensione di una directory include la dimensione di tutti i suoi elementi figlio)
  • eccetera.

È possibile aggiungere funzioni a ciascuna classe nel FileSystem per implementare le operazioni (e le persone lo hanno già fatto in passato poiché è 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, hai FileNodeun metodo che ha un metodocalculateFileColorForFunctionABC() per implementare la più recente funzionalità di visualizzazione sul file system.

Il modello visitatore (come molti modelli di progettazione) è 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 anche nel rispetto dei buoni principi di progettazione (alta coesione, basso accoppiamento ). Ritengo che sia difficile comprendere l'utilità di molti schemi fino a quando non si avverte quel dolore. Spiegare il dolore (come tentiamo di fare sopra con le funzionalità "ecc." Che vengono aggiunte) occupa spazio nella spiegazione ed è una distrazione. Per questo motivo è difficile comprendere i modelli.

Il visitatore ci consente di disaccoppiare le funzionalità sulla struttura dei dati (ad es. FileSystemNodes) Dalle strutture dei dati stessi. Il modello consente al progetto di rispettare la coesione: le classi di struttura dei dati sono più semplici (hanno meno metodi) e anche le funzionalità sono incapsulate in Visitorimplementazioni. Questo viene fatto tramite doppio dispacciamento (che è la parte complicata del modello): usando accept()metodi nelle classi struttura e visitX()metodi nelle classi Visitatore (la funzionalità):

Diagramma di classe FileSystem con visitatore applicato

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

Diagramma di classe FileSystem con visitatore applicato

Ad esempio, un PrintNameVisitorche implementa la funzionalità di elenco delle directory e un PrintSizeVisitorche implementa la versione con le dimensioni. Potremmo immaginare un giorno di avere un 'ExportXMLVisitor` che genera i dati in XML, o un altro visitatore che li genera in JSON, ecc. Potremmo anche avere un visitatore che mostra il mio albero di directory usando un linguaggio grafico come DOT , da visualizzare con un altro programma.

Come nota finale: la complessità di Visitor con la sua doppia spedizione significa che è più difficile da capire, codificare ed eseguire il debug. In breve, ha un alto fattore geek e ripropone il principio KISS. In un sondaggio condotto da ricercatori, il visitatore ha mostrato di essere un modello controverso (non c'era consenso sulla sua utilità). Alcuni esperimenti hanno anche mostrato che non ha reso il codice più facile da mantenere.


La struttura delle directory penso sia un buon modello composito, ma sono d'accordo con il tuo ultimo paragrafo.
Zar

5

A mio avviso, la quantità di lavoro per aggiungere una nuova operazione è più o meno la stessa utilizzando Visitor Patterno modificando direttamente ogni struttura di elementi. Inoltre, se dovessi aggiungere una nuova classe di elementi, diciamo Cow, l'interfaccia operativa sarà interessata e questo si propaga a tutte le classi di elementi esistenti, richiedendo quindi la ricompilazione di tutte le classi di elementi. Quindi qual è il punto?


4
Quasi ogni volta che ho usato Visitor è quando lavori con l'attraversamento di una gerarchia di oggetti. Considera un menu ad albero nidificato. Vuoi comprimere tutti i nodi. Se non si implementa il visitatore, è necessario scrivere un codice di attraversamento grafico. O con visitatore: rootElement.visit (node) -> node.collapse(). Con visitatore, ogni nodo implementa l'attraversamento grafico per tutti i suoi figli, quindi il gioco è fatto.
George Mauer,

@GeorgeMauer, il concetto di doppio dispacciamento mi ha chiarito la motivazione: o la logica dipendente dal tipo è con il tipo o il mondo del dolore. L'idea di distribuire la logica di attraversamento mi dà ancora una pausa. È più efficiente? È più mantenibile? Cosa succede se "Piega al livello N" viene aggiunto come requisito?
nik.shornikov,

L'efficienza di @ nik.shornikov non dovrebbe davvero essere un problema qui. In quasi tutte le lingue, alcune chiamate di funzione sono spese generali trascurabili. Qualunque cosa oltre a ciò è la micro-ottimizzazione. È più mantenibile? Beh, dipende. Penso che la maggior parte delle volte lo sia, a volte no. Per quanto riguarda "fold to level N". Facile passaggio in un levelsRemainingcontatore come parametro. Decrementalo prima di chiamare il livello successivo dei bambini. All'interno del tuo visitatore if(levelsRemaining == 0) return.
George Mauer,

1
@GeorgeMauer, sono totalmente d'accordo sul fatto che l'efficienza sia una preoccupazione minore. Ma la manutenibilità, ad esempio le sostituzioni della firma di accettazione, sono esattamente ciò su cui penso che la decisione dovrebbe essere ridotta.
nik.shornikov,

5

Modello visitatore come la stessa implementazione sotterranea per la programmazione di Aspect Object.

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


per menzionare Aspect Object Programming
milesma

5

Breve descrizione del modello di visitatore. Le classi che richiedono una modifica devono implementare tutte il metodo "accetta". I clienti chiamano questo metodo di accettazione per eseguire alcune nuove azioni su quella famiglia di classi estendendo così la loro funzionalità. I clienti sono in grado di utilizzare questo metodo di accettazione per eseguire una vasta gamma di nuove azioni passando una classe visitatore diversa per ogni azione specifica. Una classe visitatore contiene più metodi di visita ignorati che definiscono come ottenere la stessa azione specifica per ogni classe 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 vuoi aggiungere una nuova azione e avere quella nuova azione interamente definita all'interno di una classe visitatore anziché distribuirla su più classi.
  3. Quando il tuo capo dice che devi produrre una serie di classi che devono fare qualcosa in questo momento ! ... ma nessuno sa esattamente cosa sia quel qualcosa.

4

Non ho capito questo schema fino a quando non mi sono imbattuto nell'articolo di zio Bob e ho letto i commenti. Considera il seguente codice:

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

Anche se può sembrare buono poiché conferma la responsabilità singola viola il principio di apertura / chiusura . Ogni volta che hai un nuovo tipo di Dipendente dovrai aggiungerlo se con il controllo del tipo. E se non lo farai non lo saprai mai al momento della compilazione.

Con il modello visitatore puoi rendere il tuo codice più pulito poiché non viola il principio di apertura / chiusura e non viola la singola responsabilità. E se dimentichi di implementare la visita, non verrà compilato:

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

La magia è che mentre v.Visit(this)sembra lo stesso è in realtà diverso poiché chiama diversi sovraccarichi di visitatore.


Sì, lo trovo particolarmente utile quando si lavora con strutture ad albero, non solo liste piatte (le liste piatte sarebbero un caso speciale di un albero). Come noterai, non è terribilmente disordinato solo nelle liste, ma il visitatore può essere un salvatore poiché la navigazione tra i nodi diventa più complessa
George Mauer,

3

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 è necessario aggiungere un metodo "Walk" qui? Sarà doloroso per l'intero progetto.

Allo stesso tempo, l'aggiunta del metodo "Walk" genera nuove domande. Che dire di "Mangia" o "Dormi"? Dobbiamo davvero aggiungere un nuovo metodo alla gerarchia degli animali per ogni nuova azione o operazione che vogliamo aggiungere? È brutto e soprattutto importante, non saremo mai in grado di chiudere l'interfaccia di Animal. Quindi, con il modello visitatore, 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<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);
        }
    }
}

camminare, mangiare non sono esempi adeguati, dato che sono comuni ad entrambi Dog, così come Cat. Avresti potuto farli nella classe base in modo che siano ereditati o scegli un esempio adatto.
Abhinav Gauniyal,

i suoni sono diversi, buon campione, ma non sono sicuro che abbia qualcosa a che fare con il modello di visitatore
DAG

3

Visitatore

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

Struttura del visitatore:

inserisci qui la descrizione dell'immagine

Usa il modello Visitatore 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 Operazione da oggetti Struttura
  3. È necessario aggiungere nuove operazioni senza modificare la struttura degli oggetti
  4. Raccogli le operazioni correlate in una singola classe anziché forzarti a cambiare o derivare le classi
  5. Aggiungi funzioni alle librerie di classi per le quali non hai l'origine o non puoi modificarla

Anche se il modello di visitatore offre la flessibilità di aggiungere nuove operazioni senza modificare il codice esistente in Object, questa flessibilità presenta uno svantaggio.

Se è stato aggiunto un nuovo oggetto Visitable, è necessario modificare il codice nelle classi Visitor & 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 sugli Visitableelementi.
  3. GameVisitorè una classe che implementa Visitorinterface ( ConcreteVisitor).
  4. Ogni Visitableelemento accetta Visitore invoca un metodo di Visitorinterfaccia rilevante .
  5. Puoi trattare Gamecome Elemente giochi concreti Chess,Checkers and Ludocome ConcreteElements.

Nell'esempio sopra, Chess, Checkers and Ludosono tre diversi giochi (e Visitableclassi). In una bella giornata, ho incontrato uno scenario per registrare le statistiche di ogni gioco. Quindi, senza modificare la singola classe per implementare la funzionalità statistica, puoi centralizzare quella responsabilità in GameVisitorclasse, 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

articolo di Oodesign

articolo di fonte

per ulteriori dettagli

Decoratore

il modello consente di aggiungere il comportamento a un singolo oggetto, staticamente o dinamicamente, senza influire sul comportamento di altri oggetti della stessa classe

Articoli correlati:

Motivo decorativo per IO

Quando utilizzare il motivo decorativo?


2

Mi piace molto la descrizione e l'esempio di http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html .

Il presupposto è che hai una gerarchia di classi primarie che è stata risolta; forse proviene da un altro fornitore e non è possibile apportare modifiche a quella gerarchia. Tuttavia, il tuo intento è quello di aggiungere nuovi metodi polimorfici a quella gerarchia, il che significa che normalmente dovresti aggiungere qualcosa all'interfaccia della classe base. Quindi il dilemma è che devi aggiungere metodi alla classe base, ma non puoi toccare la classe base. Come aggirare questo?

Il modello di progettazione che risolve questo tipo di problema è chiamato "visitatore" (quello finale nel libro Modelli di progettazione) e si basa sul doppio schema di dispacciamento mostrato nell'ultima sezione.

Il modello visitatore consente di estendere l'interfaccia del tipo primario creando una gerarchia di classi separata del tipo visitatore per virtualizzare le operazioni eseguite sul tipo primario. Gli oggetti del tipo primario "accettano" semplicemente il visitatore, quindi chiamano la funzione membro associata dinamicamente al visitatore.


Sebbene tecnicamente il modello di Visitatore, questo è davvero solo un doppio invio di base dal loro esempio. Direi che l'utilità non è particolarmente visibile da solo.
George Mauer,

1

Mentre ho capito il come e quando, non ho mai capito il perché. Nel caso in cui aiuti chiunque abbia un background in un linguaggio come C ++, vuoi leggerlo molto attentamente.

Per i pigri, utilizziamo il modello visitatore perché "mentre le funzioni virtuali vengono inviate in modo dinamico in C ++, il sovraccarico delle funzioni viene eseguito staticamente" .

Oppure, in altre parole, per assicurarsi che CollideWith (ApolloSpacecraft &) venga chiamato quando si passa un riferimento SpaceShip che è effettivamente associato a un oggetto 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
L'uso del dispacciamento dinamico nel modello di visitatore mi ha completamente sconcertato. Gli usi suggeriti del modello descrivono la ramificazione che potrebbe essere eseguita in fase di compilazione. Apparentemente questi casi starebbero meglio con un modello di funzione.
Prassolitico il

0

Grazie per la fantastica spiegazione di @Federico A. Ramponi , l'ho appena realizzato in versione java . Spero possa essere utile.

Inoltre, come ha sottolineato @Konrad Rudolph , in realtà si tratta di una doppia spedizione che utilizza due istanze concrete insieme per determinare i metodi di runtime.

Quindi in realtà non è necessario creare un'interfaccia comune per l' esecutore dell'operazione , purché l' interfaccia di operazione sia definita correttamente.

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

Come ci si aspetta, un'interfaccia comune ci porterà più chiarezza, anche se in realtà non è la parte essenziale in questo modello.

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

la tua domanda è quando sapere:

non codifico prima con il modello visitatore. codifico standard e aspetto che si verifichi la necessità, quindi refactoring. quindi supponiamo che tu abbia più sistemi di pagamento installati uno alla volta. Al momento del checkout potresti avere molte condizioni if ​​(o instanceOf), ad esempio:

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

ora immagino di avere 10 metodi di pagamento, diventa brutto. Quindi, quando vedi quel tipo di modello che si presenta, il visitatore entra in mano per separare tutto ciò e finisci per chiamare qualcosa di simile in seguito:

new PaymentCheckoutVistor(paymentType).visit()

Puoi vedere come implementarlo dal numero di esempi qui, ti sto solo mostrando un caso d'uso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.