Qual è lo scopo del metodo accept () nel pattern Visitor?


88

Si parla molto sul disaccoppiamento degli algoritmi dalle classi. Ma una cosa rimane da parte non spiegata.

Usano il visitatore in questo modo

abstract class Expr {
  public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}

class ExprVisitor extends Visitor{
  public Integer visit(Num num) {
    return num.value;
  }

  public Integer visit(Sum sum) {
    return sum.getLeft().accept(this) + sum.getRight().accept(this);
  }

  public Integer visit(Prod prod) {
    return prod.getLeft().accept(this) * prod.getRight().accept(this);
  }

Invece di chiamare direttamente visit (element), Visitor chiede all'elemento di chiamare il suo metodo visit. Contrasta l'idea dichiarata di inconsapevolezza di classe nei confronti dei visitatori.

PS1 Spiega con parole tue o indica una spiegazione esatta. Perché due risposte che ho ricevuto si riferiscono a qualcosa di generale e di incerto.

PS2 La mia ipotesi: poiché getLeft()restituisce il basic Expression, la chiamata visit(getLeft())risulterebbe visit(Expression), mentre la getLeft()chiamata visit(this)risulterà in un'altra chiamata di visita, più appropriata. Quindi, accept()esegue la conversione del tipo (ovvero il casting).

Pattern Matching = Visitor Pattern on Steroid di PS3 Scala mostra quanto sia più semplice il pattern Visitor senza il metodo di accettazione. Wikipedia aggiunge a questa affermazione : collegando un documento che mostra "che i accept()metodi non sono necessari quando la riflessione è disponibile; introduce il termine 'Walkabout' per la tecnica."



Dice "quando il visitatore chiama accetta, la chiamata viene inviata in base al tipo di chiamato. Quindi il chiamato richiama il metodo di visita specifico del tipo di visitatore e questa chiamata viene inviata in base al tipo effettivo di visitatore." In altre parole, afferma la cosa che mi confonde. Per questo motivo, puoi essere più specifico?
Val

Risposte:


155

I modelli visit/ acceptcostrutti del visitatore sono un male necessario a causa della semantica dei linguaggi C # (C #, Java, ecc.). L'obiettivo del pattern visitatore è utilizzare il doppio invio per instradare la chiamata come ci si aspetterebbe dalla lettura del codice.

Normalmente quando viene utilizzato il pattern del visitatore, viene coinvolta una gerarchia di oggetti in cui tutti i nodi derivano da un Nodetipo di base , denominato d'ora in poi Node. Istintivamente, lo scriveremmo così:

Node root = GetTreeRoot();
new MyVisitor().visit(root);

Qui sta il problema. Se la nostra MyVisitorclasse fosse definita come la seguente:

class MyVisitor implements IVisitor {
  void visit(CarNode node);
  void visit(TrainNode node);
  void visit(PlaneNode node);
  void visit(Node node);
}

Se, in fase di esecuzione, indipendentemente dal tipo effettivoroot , la nostra chiamata andasse in overload visit(Node node). Questo sarebbe vero per tutte le variabili dichiarate di tipo Node. Perchè è questo? Perché Java e altri linguaggi simili a C considerano solo il tipo statico , o il tipo con cui è dichiarata la variabile, del parametro quando si decide quale sovraccarico chiamare. Java non fa il passo in più per chiedere, per ogni chiamata di metodo, in fase di esecuzione, "Ok, qual è il tipo dinamico di root? Oh, capisco. È un TrainNode. Vediamo se c'è qualche metodo in MyVisitorcui accetta un parametro di tipoTrainNode... ". Il compilatore, in fase di compilazione, determina qual è il metodo che verrà chiamato. (Se Java effettivamente ispezionasse i tipi dinamici degli argomenti, le prestazioni sarebbero piuttosto terribili).

Java ci fornisce uno strumento per prendere in considerazione il tipo di runtime (cioè dinamico) di un oggetto quando viene chiamato un metodo: invio del metodo virtuale . Quando chiamiamo un metodo virtuale, la chiamata va effettivamente a una tabella in memoria che consiste di puntatori a funzione. Ogni tipo ha una tabella. Se un particolare metodo viene sovrascritto da una classe, la voce della tabella delle funzioni di quella classe conterrà l'indirizzo della funzione sovrascritta. Se la classe non sovrascrive un metodo, conterrà un puntatore all'implementazione della classe base. Ciò comporta ancora un sovraccarico delle prestazioni (ogni chiamata al metodo fondamentalmente dereferenzia due puntatori: uno che punta alla tabella delle funzioni del tipo e un altro alla funzione stessa), ma è ancora più veloce di dover ispezionare i tipi di parametro.

L'obiettivo del pattern del visitatore è realizzare un doppio invio : non viene considerato solo il tipo di target della chiamata ( MyVisitortramite metodi virtuali), ma anche il tipo di parametro (che tipo di target Nodestiamo guardando)? Il pattern Visitor ci consente di farlo tramite la combinazione visit/ accept.

Modificando la nostra linea in questo:

root.accept(new MyVisitor());

Possiamo ottenere ciò che vogliamo: tramite l'invio del metodo virtuale, inseriamo la chiamata accept () corretta come implementata dalla sottoclasse - nel nostro esempio con TrainElement, entreremo TrainElementnell'implementazione di accept():

class TrainNode extends Node implements IVisitable {
  void accept(IVisitor v) {
    v.visit(this);
  }
}

Cosa sa il compilatore a questo punto, nell'ambito di TrainNodes accept? Sa che il tipo statico di thisè aTrainNode . Questo è un importante frammento di informazioni aggiuntive di cui il compilatore non era a conoscenza nell'ambito del nostro chiamante: lì, tutto ciò che sapeva rootera che si trattava di un file Node. Ora il compilatore sa che this( root) non è solo un Node, ma in realtà è un file TrainNode. Di conseguenza, l'una linea trovato all'interno accept(): v.visit(this), significa qualcosa di completamente diverso. Il compilatore ora cercherà un sovraccarico visit()che richiede un file TrainNode. Se non riesce a trovarne uno, compilerà la chiamata a un overload che richiede un fileNode. Se nessuno dei due esiste, riceverai un errore di compilazione (a meno che tu non abbia un sovraccarico che tenga object). L'esecuzione entrerà così in ciò che avevamo inteso fin dall'inizio: MyVisitorl'implementazione di visit(TrainNode e). Non erano necessari calchi e, soprattutto, non era necessaria alcuna riflessione. Pertanto, il sovraccarico di questo meccanismo è piuttosto basso: consiste solo di riferimenti a puntatori e nient'altro.

Hai ragione nella tua domanda: possiamo usare un cast e ottenere il comportamento corretto. Tuttavia, spesso, non sappiamo nemmeno che tipo sia Node. Prendi il caso della seguente gerarchia:

abstract class Node { ... }
abstract class BinaryNode extends Node { Node left, right; }
abstract class AdditionNode extends BinaryNode { }
abstract class MultiplicationNode extends BinaryNode { }
abstract class LiteralNode { int value; }

E stavamo scrivendo un semplice compilatore che analizza un file sorgente e produce una gerarchia di oggetti conforme alla specifica sopra. Se stessimo scrivendo un interprete per la gerarchia implementata come Visitatore:

class Interpreter implements IVisitor<int> {
  int visit(AdditionNode n) {
    int left = n.left.accept(this);
    int right = n.right.accept(this); 
    return left + right;
  }
  int visit(MultiplicationNode n) {
    int left = n.left.accept(this);
    int right = n.right.accept(this);
    return left * right;
  }
  int visit(LiteralNode n) {
    return n.value;
  }
}

Il casting non ci porterebbe molto lontano, dal momento che non conosciamo i tipi di lefto rightnei visit()metodi. Il nostro parser molto probabilmente restituirebbe anche un oggetto di tipo Nodeche punta alla radice della gerarchia, quindi non possiamo nemmeno eseguire il cast in sicurezza. Quindi il nostro semplice interprete può assomigliare a:

Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);

Il pattern del visitatore ci permette di fare qualcosa di molto potente: data una gerarchia di oggetti, ci permette di creare operazioni modulari che operano sulla gerarchia senza dover richiedere di inserire il codice nella classe della gerarchia stessa. Il pattern del visitatore è ampiamente utilizzato, ad esempio, nella costruzione del compilatore. Dato l'albero della sintassi di un particolare programma, vengono scritti molti visitatori che operano su quell'albero: il controllo del tipo, le ottimizzazioni, l'emissione del codice macchina sono tutti normalmente implementati come visitatori diversi. Nel caso del visitatore di ottimizzazione, può anche produrre un nuovo albero di sintassi dato l'albero di input.

Ha i suoi svantaggi, ovviamente: se aggiungiamo un nuovo tipo nella gerarchia, dobbiamo anche aggiungere un visit()metodo per quel nuovo tipo IVisitornell'interfaccia e creare implementazioni stub (o complete) in tutti i nostri visitatori. Dobbiamo anche aggiungere anche il accept()metodo, per i motivi sopra descritti. Se le prestazioni non significano molto per te, ci sono soluzioni per scrivere ai visitatori senza bisogno di accept(), ma normalmente implicano una riflessione e quindi possono incorrere in un sovraccarico abbastanza grande.


5
L'elemento Java effettivo # 41 include questo avviso: " evitare situazioni in cui lo stesso insieme di parametri può essere passato a diversi sovraccarichi mediante l'aggiunta di cast. " Il accept()metodo diventa necessario quando questo avviso viene violato nel Visitatore.
jaco0646

" Normalmente quando viene utilizzato il pattern del visitatore, è coinvolta una gerarchia di oggetti in cui tutti i nodi derivano da un tipo di nodo di base " questo non è assolutamente necessario in C ++. Vedi Boost.Variant, Eggs.Variant
Jean-Michaël Celerier

Mi sembra che in java non abbiamo davvero bisogno del metodo di accettazione perché in java chiamiamo sempre il metodo di tipo più specifico
Gilad Baruchian

1
Wow, questa è stata una spiegazione fantastica. È illuminante vedere che tutte le ombre del pattern sono dovute ai limiti del compilatore e ora si rivela chiaro grazie a te.
Alfonso Nishikawa,

@GiladBaruchian, il compilatore genera una chiamata al metodo di tipo più specifico che il compilatore può determinare.
mmw

15

Ovviamente sarebbe sciocco se questo fosse l' unico modo in cui viene implementato Accept.

Ma non è.

Ad esempio, i visitatori sono davvero molto utili quando si tratta di gerarchie, nel qual caso l'implementazione di un nodo non terminale potrebbe essere qualcosa del genere

interface IAcceptVisitor<T> {
  void Accept(IVisit<T> visitor);
}
class HierarchyNode : IAcceptVisitor<HierarchyNode> {
  public void Accept(IVisit<T> visitor) {
    visitor.visit(this);
    foreach(var n in this.children)
      n.Accept(visitor);
  }

  private IEnumerable<HierarchyNode> children;
  ....
}

Vedi? Ciò che descrivi come stupido è la soluzione per attraversare le gerarchie.

Ecco un articolo molto più lungo e approfondito che mi ha fatto capire il visitatore .

Modifica: per chiarire: il Visitmetodo del visitatore contiene la logica da applicare a un nodo. Il Acceptmetodo del nodo contiene la logica su come navigare verso i nodi adiacenti. Il caso in cui si effettua solo un doppio invio è un caso speciale in cui semplicemente non ci sono nodi adiacenti a cui navigare.


7
La tua spiegazione non spiega perché dovrebbe essere responsabilità del metodo Node piuttosto che del metodo visit () appropriato del Visitatore iterare i bambini? Vuoi dire che l'idea principale è condividere il codice di attraversamento della gerarchia quando abbiamo bisogno degli stessi schemi di visita per visitatori diversi? Non vedo alcun suggerimento dalla carta consigliata.
Val

1
Dire che accettare è un bene per l'attraversamento di routine è ragionevole e vale la pena per la popolazione generale. Ma ho preso il mio esempio da qualcun altro "Non riuscivo a capire il pattern Visitor finché non ho letto andymaleh.blogspot.com/2008/04/… ". Né questo esempio né Wikipedia né altre risposte menzionano il vantaggio della navigazione. Tuttavia, tutti richiedono questa stupida accettazione (). Ecco perché poni la mia domanda: perché?
Val

1
@Val - cosa intendi? Non sono sicuro di quello che stai chiedendo. Non posso parlare per altri articoli in quanto quelle persone hanno opinioni diverse su questa roba, ma dubito che siamo in disaccordo. In generale, nel calcolo molti problemi possono essere mappati alle reti, quindi un utilizzo potrebbe non avere nulla a che fare con i grafici in superficie, ma in realtà è un problema molto simile.
George Mauer

1
Fornire un esempio di dove un metodo potrebbe essere utile non risponde alla domanda sul perché il metodo sia obbligatorio. Poiché la navigazione non è sempre necessaria, il metodo accept () non è sempre utile per la visita. Dovremmo essere in grado di raggiungere i nostri obiettivi senza di essa, quindi. Tuttavia, è obbligatorio. Significa che c'è una ragione più forte per introdurre accept () in ogni patter di visitatori che "a volte è utile". Cosa non è chiaro nella mia domanda? Se non cerchi di capire perché Wikipedia cerca modi per sbarazzarti di accettare, non sei interessato a capire la mia domanda.
Val

1
@Val Il documento che collegano a "The Essence of the Visitor Pattern" nota la stessa separazione tra navigazione e funzionamento nel suo abstract che ho dato. Stanno semplicemente dicendo che l'implementazione GOF (che è ciò di cui stai chiedendo) ha alcune limitazioni e fastidi che possono essere rimossi con l'uso della riflessione, quindi introducono il modello Walkabout. Questo è certamente utile e può fare gran parte delle stesse cose che il visitatore può fare, ma è un sacco di codice abbastanza sofisticato e (in una lettura superficiale) perde alcuni vantaggi della sicurezza dei tipi. È uno strumento per la cassetta degli attrezzi ma più pesante del visitatore
George Mauer

0

Lo scopo del modello Visitatore è garantire che gli oggetti sappiano quando il visitatore ha finito di utilizzarli e se ne è andato, in modo che le classi possano eseguire le operazioni di pulizia necessarie in seguito. Consente inoltre alle classi di esporre i loro interni "temporaneamente" come parametri 'ref' e sapere che gli interni non saranno più esposti una volta che il visitatore se ne sarà andato. Nei casi in cui non è necessaria alcuna pulizia, il pattern del visitatore non è particolarmente utile. Le classi che non fanno nessuna di queste cose potrebbero non trarre vantaggio dal modello visitatore, ma il codice scritto per utilizzare il modello visitatore sarà utilizzabile con classi future che potrebbero richiedere la pulizia dopo l'accesso.

Ad esempio, supponiamo che uno abbia una struttura dati contenente molte stringhe che dovrebbero essere aggiornate atomicamente, ma la classe che detiene la struttura dati non sa esattamente quali tipi di aggiornamenti atomici dovrebbero essere eseguiti (es. Se un thread vuole sostituire tutte le occorrenze di " X ", mentre un altro thread vuole sostituire qualsiasi sequenza di cifre con una sequenza numericamente superiore di uno, le operazioni di entrambi i thread dovrebbero riuscire; se ogni thread legge semplicemente una stringa, esegue i suoi aggiornamenti e la riscrive, il secondo thread riscrivere la sua stringa sovrascriverebbe la prima). Un modo per ottenere ciò sarebbe fare in modo che ogni thread acquisisca un blocco, ne esegua l'operazione e rilasci il blocco. Sfortunatamente, se le serrature sono esposte in questo modo,

Il pattern Visitor offre (almeno) tre approcci per evitare questo problema:

  1. Può bloccare un record, chiamare la funzione fornita e quindi sbloccare il record; il record potrebbe essere bloccato per sempre se la funzione fornita cade in un ciclo infinito, ma se la funzione fornita restituisce o genera un'eccezione, il record verrà sbloccato (può essere ragionevole contrassegnare il record come non valido se la funzione genera un'eccezione; lasciare è bloccato probabilmente non è una buona idea). Si noti che è importante che se la funzione chiamata tenta di acquisire altri blocchi, potrebbe verificarsi un deadlock.
  2. Su alcune piattaforme, può passare una posizione di archiviazione che contiene la stringa come parametro "ref". Quella funzione potrebbe quindi copiare la stringa, calcolare una nuova stringa in base alla stringa copiata, provare a CompareExchange la vecchia stringa in quella nuova e ripetere l'intero processo se il CompareExchange non riesce.
  3. Può creare una copia della stringa, chiamare la funzione fornita sulla stringa, quindi utilizzare lo stesso CompareExchange per tentare di aggiornare l'originale e ripetere l'intero processo se il CompareExchange fallisce.

Senza il pattern del visitatore, l'esecuzione di aggiornamenti atomici richiederebbe l'esposizione di blocchi e il rischio di guasti se il software chiamante non riesce a seguire un rigoroso protocollo di blocco / sblocco. Con il pattern Visitor, gli aggiornamenti atomici possono essere eseguiti in modo relativamente sicuro.


2
1. la visita implica che hai accesso solo ai metodi pubblici di visitato, quindi è necessario rendere le serrature interne accessibili per il pubblico per essere utile con il visitatore. 2 / Nessuno degli esempi che ho visto prima implica che il Visitatore debba essere utilizzato per cambiare lo stato di visitato. 3. "Con il tradizionale VisitorPattern, si può solo determinare quando stiamo entrando in un nodo. Non sappiamo se abbiamo lasciato il nodo precedente prima di entrare nel nodo corrente." Come si sblocca solo visit invece di visitEnter and visitLeave? Infine, ho chiesto informazioni sulle applicazioni di accpet () piuttosto che di Visitor.
Val

Forse non sono del tutto al passo con la terminologia per i modelli, ma il "modello visitatore" sembra assomigliare a un approccio che ho usato in cui X passa a Y un delegato, al quale Y può quindi passare informazioni che devono essere valide solo come finché il delegato è in esecuzione. Forse quel modello ha un altro nome?
supercat

2
Questo è un interessante applicazione del modello visitatore di un problema specifico, ma non descrive il modello stesso o rispondere alla domanda iniziale. "Nei casi in cui non è necessaria alcuna pulizia, il pattern dei visitatori non è particolarmente utile." Questa affermazione è decisamente falsa e si riferisce solo al tuo problema specifico e non al modello in generale.
Tony O'Hagan

0

Le classi che richiedono modifiche devono tutte implementare il metodo "accept". I client 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 un'ampia gamma di nuove azioni passando una diversa classe visitatore per ogni azione specifica. Una classe visitatore contiene più metodi di visita sostituiti che definiscono come ottenere la stessa azione specifica per ogni classe all'interno della famiglia. Questi metodi di visita vengono passati a un'istanza su cui lavorare.

I visitatori sono utili se si aggiungono, alterano o rimuovono frequentemente funzionalità a una famiglia stabile di classi perché ogni elemento di funzionalità è definito separatamente in ciascuna classe visitatore e le classi stesse non devono essere modificate. Se la famiglia di classi non è stabile, il modello dei visitatori potrebbe essere di minore utilità, perché molti visitatori devono cambiare ogni volta che una classe viene aggiunta o rimossa.


-1

Un buon esempio è nella compilazione del codice sorgente:

interface CompilingVisitor {
   build(SourceFile source);
}

I clienti possono implementare una JavaBuilder, RubyBuilder, XMLValidator, ecc e l'implementazione per la raccolta e visitare tutti i file di origine in un progetto non ha bisogno di cambiamento.

Questo sarebbe un cattivo modello se hai classi separate per ogni tipo di file sorgente:

interface CompilingVisitor {
   build(JavaSourceFile source);
   build(RubySourceFile source);
   build(XMLSourceFile source);
}

Dipende dal contesto e dalle parti del sistema che desideri estendere.


L'ironia è che VisitorPattern ci offre di usare il cattivo pattern. Dice che dobbiamo definire un metodo di visita per ogni tipo di nodo che visiterà. In secondo luogo, non è chiaro quali siano i tuoi esempi buoni o cattivi? Come sono correlati alla mia domanda?
Val
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.