Utilizzo del modello visitatore con gerarchia di oggetti di grandi dimensioni


12

Contesto

Ho usato con una gerarchia di oggetti (un albero delle espressioni) un modello di visitatore "pseudo" (pseudo, poiché in esso non viene utilizzato il doppio dispacciamento):

 public interface MyInterface
 {
      void Accept(SomeClass operationClass);
 }

 public class MyImpl : MyInterface 
 {
      public void Accept(SomeClass operationClass)
      {   
           operationClass.DoSomething();
           operationClass.DoSomethingElse();
           // ... and so on ...
      }
 }

Questo design è stato, comunque, discutibile, abbastanza comodo poiché il numero di implementazioni di MyInterface è significativo (~ 50 o più) e non ho avuto bisogno di aggiungere ulteriori operazioni.

Ogni implementazione è unica (è un'espressione o un operatore diverso) e alcuni sono compositi (cioè nodi di operatori che conterranno altri nodi operatore / foglia).

Traversal viene attualmente eseguito chiamando l'operazione Accept sul nodo radice dell'albero, che a sua volta chiama Accept su ciascuno dei suoi nodi figlio, che a sua volta ... e così via ...

Ma è giunto il momento in cui devo aggiungere una nuova operazione , come la stampa carina:

 public class MyImpl : MyInterface 
 {
      // Property does not come from MyInterface
      public string SomeProperty { get; set; }

      public void Accept(SomeClass operationClass)
      {   
           operationClass.DoSomething();
           operationClass.DoSomethingElse();
           // ... and so on ...
      }

      public void Accept(SomePrettyPrinter printer)
      {
           printer.PrettyPrint(this.SomeProperty);
      }
 }    

Praticamente vedo due opzioni:

  • Mantenere lo stesso design, aggiungendo un nuovo metodo per la mia operazione a ciascuna classe derivata, a spese della manutenibilità (non un'opzione, IMHO)
  • Usa il modello "vero" di Visitatori, a scapito dell'estensibilità (non un'opzione, poiché mi aspetto di avere più implementazioni in arrivo ...), con circa 50+ sovraccarichi del metodo Visit, ognuno corrispondente a un'implementazione specifica ?

Domanda

Consiglieresti di usare il modello Visitatore? C'è qualche altro modello che potrebbe aiutare a risolvere questo problema?


1
Forse una catena di decoratori sarebbe più appropriata?
MattDavey,

alcune domande: in che modo differiscono queste implementazioni? qual è la struttura della gerarchia? ed è sempre la stessa struttura? devi sempre attraversare la struttura nello stesso ordine?
jk.

@MattDavey: quindi consiglieresti di avere un decoratore per implementazione e operazione?
T. Fabre,

2
@ T.Fabre è difficile da dire. Ci sono oltre 50 implementatori di MyInterface.. tutte queste classi hanno un'implementazione unica di DoSomethinge DoSomethingElse? Non vedo dove la tua classe di visitatori attraversi effettivamente la gerarchia - sembra più simile a un facadeal momento ..
MattDavey

anche quale versione di C # è. hai lambdas? o linq? a vostra disposizione
jk.

Risposte:


13

Ho usato il modello di visitatore per rappresentare gli alberi delle espressioni nel corso di oltre 10 anni su sei progetti su larga scala in tre linguaggi di programmazione, e sono molto contento del risultato. Ho trovato un paio di cose che hanno reso l'applicazione del modello molto più semplice:

Non utilizzare sovraccarichi nell'interfaccia del visitatore

Inserisci il tipo nel nome del metodo, ovvero usa

IExpressionVisitor {
    void VisitPrimitive(IPrimitiveExpression expr);
    void VisitComposite(ICompositeExpression expr);
}

piuttosto che

IExpressionVisitor {
    void Visit(IPrimitiveExpression expr);
    void Visit(ICompositeExpression expr);
}

Aggiungi un metodo "cattura sconosciuto" alla tua interfaccia visitatore.

Ciò consentirebbe agli utenti che non possono modificare il codice:

IExpressionVisitor {
    void VisitPrimitive(IPrimitiveExpression expr);
    void VisitComposite(ICompositeExpression expr);
    void VisitExpression(IExpression expr);
};

Ciò consentirebbe loro di costruire le proprie implementazioni IExpressione IVisitorche "comprenda" le loro espressioni utilizzando le informazioni sul tipo di runtime nell'implementazione del loro VisitExpressionmetodo catch-all .

Fornire un'implementazione predefinita IVisitordell'interfaccia do-nothing

Ciò consentirebbe agli utenti che hanno bisogno di gestire un sottoinsieme di tipi di espressioni per costruire i loro visitatori più velocemente e rendere il loro codice immune da te aggiungendo altri metodi IVisitor. Ad esempio, scrivere un visitatore che raccoglie tutti i nomi delle variabili dalle espressioni diventa un compito facile e il codice non si interromperà anche se in seguito aggiungi un gruppo di nuovi tipi di espressioni IVisitor.


2
Puoi chiarire perché dici Do not use overloads in the interface of the visitor?
Steven Evers,

1
Puoi spiegare perché non consigli di usare i sovraccarichi? Ho letto da qualche parte (su oodesign.com, in realtà) che non importa se uso sovraccarichi o meno. C'è un motivo specifico per cui preferisci quel design?
T. Fabre,

2
@ T.Fabre Non importa in termini di velocità, ma importa in termini di leggibilità. La risoluzione del metodo in due delle tre lingue in cui ho implementato questo ( Java e C #) richiede un passaggio di runtime per scegliere tra i potenziali sovraccarichi, rendendo il codice con un numero enorme di sovraccarichi un po 'più difficile da leggere. Anche il refactoring del codice diventa più facile, perché scegliere il metodo che desideri modificare diventa un'attività banale.
dasblinkenlight,

@SnOrfus Vedi la mia risposta a T.Fabre sopra.
dasblinkenlight,

@dasblinkenlight C # ora offre dinamiche per consentire al runtime di decidere quale metodo sovraccarico deve essere utilizzato (non in fase di compilazione). C'è ancora qualche motivo per non usare il sovraccarico?
Tintenfiisch,
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.