2009-06-12 19 views
44

Sto cercando un'alternativa al modello di visitatore. Permettetemi di concentrarmi su un paio di aspetti pertinenti del modello, tralasciando i dettagli non importanti. Userò un esempio di forma (scusate!):Alternativa al modello di visitatore?

  1. Hai una gerarchia di oggetti che implementano l'interfaccia IShape
  2. Si dispone di un certo numero di operazioni globali che devono essere eseguite su tutti gli oggetti nella gerarchia , per esempio Draw, WriteToXml ecc ...
  3. Si è tentati di immergersi direttamente e aggiungere un metodo Draw() e WriteToXml() all'interfaccia IShape. Questa non è necessariamente una buona cosa: ogni volta che desideri aggiungere una nuova operazione che deve essere eseguita su tutte le forme, ogni classe derivata da IShape deve essere modificata
  4. Implementazione di un visitatore per ogni operazione, ad esempio un visitatore di Draw o un visitatore di WirteToXml incapsula tutto il codice per quell'operazione in una classe. L'aggiunta di una nuova operazione comporta la creazione di una nuova classe visitatore che esegue l'operazione su tutti i tipi di IShape
  5. Quando è necessario aggiungere una nuova classe derivata da IShape, si ha essenzialmente lo stesso problema di 3 - tutte le classi di visitatori devono essere modificate per aggiungere un metodo per gestire il nuovo tipo derivato da IShape

La maggior parte dei luoghi in cui si legge lo stato del modello di visitatore che il punto 5 è praticamente il criterio principale per il motivo e il motivo completamente d'accordo. Se il numero di classi derivate da IShape è fisso, questo può essere un approccio abbastanza elegante.

Quindi, il problema è quando viene aggiunta una nuova classe derivata da IShape: ogni implementazione di visitatore deve aggiungere un nuovo metodo per gestire quella classe. Questo è, nel migliore dei casi, spiacevole e, nel peggiore dei casi, non possibile e dimostra che questo modello non è realmente progettato per far fronte a tali cambiamenti.

Quindi, la domanda è: qualcuno ha incontrato approcci alternativi per gestire questa situazione?

+0

Solo una nota a parte, in quanto è improbabile che sia possibile modificare la lingua: Esistono lingue che supportano direttamente più funzioni generiche di invio. – Svante

+6

Ottima domanda. Volevo solo fornire un contrappunto. A volte il tuo problema con (5) può essere una buona cosa. Uso il pattern visitor quando ho alcune funzionalità che devono essere aggiornate quando viene definito un nuovo sottotipo IShape. Ho un'interfaccia IShapeVisitor che definisce quali metodi sono necessari. Finché l'interfaccia viene aggiornata con il nuovo sottotipo, il mio codice non viene creato fino a quando non viene aggiornata la funzionalità critica. Per alcune situazioni, questo può essere molto utile. – oillio

+1

Sono d'accordo con @oillio, ma potreste applicarlo come metodo astratto su IShape. Ciò che il modello Visitor ti compra in un linguaggio OO puro è la località della funzione (rispetto alla località della classe) e quindi una separazione delle preoccupazioni.In ogni caso, se si desidera forzare l'aggiunta di nuovi tipi da riesaminare attentamente, lo schema del visitatore deve essere esplicitamente interrotto in fase di compilazione! –

risposta

13

Si potrebbe desiderare di dare un'occhiata al Strategy pattern. Questo ti dà ancora una separazione di preoccupazioni pur essendo ancora in grado di aggiungere nuove funzionalità senza dover cambiare ogni classe nella tua gerarchia.

class AbstractShape 
{ 
    IXmlWriter _xmlWriter = null; 
    IShapeDrawer _shapeDrawer = null; 

    public AbstractShape(IXmlWriter xmlWriter, 
       IShapeDrawer drawer) 
    { 
     _xmlWriter = xmlWriter; 
     _shapeDrawer = drawer; 
    } 

    //... 
    public void WriteToXml(IStream stream) 
    { 
     _xmlWriter.Write(this, stream); 

    } 

    public void Draw() 
    { 
     _drawer.Draw(this); 
    } 

    // any operation could easily be injected and executed 
    // on this object at run-time 
    public void Execute(IGeneralStrategy generalOperation) 
    { 
     generalOperation.Execute(this); 
    } 
} 

Maggiori informazioni sono in questa discussione relativa:

Should an object write itself out to a file, or should another object act on it to perform I/O?

+0

L'ho contrassegnato come la risposta alla mia domanda poiché penso che questo, o qualche piccola variazione su di esso, probabilmente si adatta a ciò che voglio fare. Per chiunque abbia un interesse, ho aggiunto una "risposta" che descrive alcuni dei miei pensieri sul problema – Steg

+0

ok - ho cambiato idea riguardo la risposta - cercherò di condensare in un commento (successivo) – Steg

+2

Penso che ci sia è un conflitto fondamentale qui - se hai un sacco di cose e un sacco di azioni che possono essere eseguite su queste cose allora aggiungere una nuova cosa significa che devi definire l'effetto di tutte le azioni su di esso e viceversa - non c'è scampo Questo. Il visitatore offre un modo molto semplice ed elegante di aggiungere nuove azioni a scapito di rendere difficile aggiungere nuove cose. Se questo vincolo deve essere rilassato, devi pagare. Speravo che potesse esserci una soluzione che avesse l'eleganza e la semplicità del visitatore, ma come sospettavo, non credo che esista ... continua ... – Steg

13

c'è il "modello visitatore con default", in cui si fa il modello visitatore come normale, ma poi definire una classe astratta che implementa la tua classe IShapeVisitor delegando tutto a un metodo astratto con la firma visitDefault(IShape).

Quindi, quando si definisce un visitatore, estendere questa classe astratta anziché implementare direttamente l'interfaccia. È possibile ignorare i metodi visit * di cui si conosce in quel momento e fornire un valore predefinito accettabile. Tuttavia, se non c'è davvero alcun modo per capire il comportamento di default ragionevole prima del tempo, si dovrebbe semplicemente implementare l'interfaccia direttamente.

Quando si aggiunge una nuova sottoclasse IShape, quindi, si corregge la classe astratta per delegare al suo metodo visitDefault e ogni visitatore che ha specificato un comportamento predefinito ottiene quel comportamento per il nuovo IShape.

Una variazione su questo se le vostre classi IShape rientrano naturalmente in una gerarchia significa rendere la classe astratta delegata attraverso diversi metodi diversi; per esempio, un DefaultAnimalVisitor potrebbe fare:

public abstract class DefaultAnimalVisitor implements IAnimalVisitor { 
    // The concrete animal classes we have so far: Lion, Tiger, Bear, Snake 
    public void visitLion(Lion l) { visitFeline(l); } 
    public void visitTiger(Tiger t) { visitFeline(t); } 
    public void visitBear(Bear b) { visitMammal(b); } 
    public void visitSnake(Snake s) { visitDefault(s); } 

    // Up the class hierarchy 
    public void visitFeline(Feline f) { visitMammal(f); } 
    public void visitMammal(Mammal m) { visitDefault(m); } 

    public abstract void visitDefault(Animal a); 
} 

Questo consente di definire i visitatori che specificano il loro comportamento a qualsiasi livello di specificità che si desidera.

Sfortunatamente, non c'è modo di evitare di fare qualcosa per specificare come si comportano i visitatori con una nuova classe - o è possibile impostare un valore predefinito prima del tempo o non si può. (Vedere anche il secondo pannello di this cartoon)

6

Manutenzione di un software CAD/CAM per la macchina per il taglio dei metalli. Quindi ho una certa esperienza con questi problemi.

Quando abbiamo convertito per la prima volta il nostro software (è stato rilasciato per la prima volta nel 1985!) Su un oggetto orientato al design, ho fatto esattamente quello che non ti piace. Gli oggetti e le interfacce avevano Draw, WriteToFile, ecc. Scoprire e leggere i Design Pattern a metà della conversione aiutava molto ma c'erano ancora molti odori di codice cattivo.

Alla fine mi sono reso conto che nessuno di questi tipi di operazioni era veramente la preoccupazione dell'oggetto. Ma piuttosto i vari sottosistemi che dovevano fare le varie operazioni. Ho gestito questo utilizzando quello che ora viene chiamato un oggetto comando Passive View e un'interfaccia ben definita tra i livelli del software.

Il nostro software è strutturato fondamentalmente come questo

  • Le forme di attuazione varie forme Interface. Queste forme sono una cosa shell che passa eventi al livello dell'interfaccia utente.
  • Livello UI che riceve eventi e manipola moduli tramite l'interfaccia Modulo.
  • Il livello UI eseguirà i comandi che implementano l'interfaccia Command
  • L'oggetto UI ha interfacce proprie che il comando può interagire con.
  • I comandi ricevono le informazioni di cui hanno bisogno, lo elaborano, manipolano il modello e quindi segnalano agli oggetti dell'interfaccia utente che quindi eseguono qualsiasi operazione necessaria con i moduli.
  • Infine i modelli che contengono i vari oggetti del nostro sistema. Come programmi forma, percorsi di taglio, tavolo da taglio e fogli di metallo.

Così Disegno viene gestito nel livello dell'interfaccia utente. Abbiamo diversi software per diverse macchine. Quindi, mentre tutti i nostri software condividono lo stesso modello e riutilizzano molti degli stessi comandi. Gestiscono cose come disegnare in modo molto diverso. Ad esempio un tavolo da taglio è diverso per una macchina router rispetto a una macchina che utilizza una torcia al plasma nonostante entrambi siano essenzialmente un gigantesco tavolo piatto X-Y. Questo perché, come le macchine, le due macchine sono costruite in modo diverso in modo tale da creare una differenza visiva per il cliente.

Per quanto riguarda le forme quello che facciamo è la seguente

Abbiamo programmi di forma che producono percorsi di taglio attraverso i parametri immessi. Il percorso di taglio conosce quale programma di forme è stato prodotto. Tuttavia un percorso di taglio non è una forma. Sono solo le informazioni necessarie per disegnare sullo schermo e tagliare la forma. Una ragione per questa progettazione è che i percorsi di taglio possono essere creati senza un programma di forma quando vengono importati da un'app esterna.

Questo design ci consente di separare il design del percorso di taglio dal design della forma che non è sempre la stessa cosa. Nel tuo caso probabilmente tutto ciò che ti serve per pacchettizzare sono le informazioni necessarie per disegnare la forma.

Ogni programma forma ha un numero di viste che implementano un'interfaccia IShapeView. Attraverso l'interfaccia IShapeView il programma di forma può dire al modulo di forma generico che abbiamo come impostarlo per mostrare i parametri di quella forma. La forma di forma generica implementa un'interfaccia IShapeForm e si registra con l'oggetto ShapeScreen. L'oggetto ShapeScreen si registra con il nostro oggetto applicazione. Le viste di forma usano qualsiasi schermo di forma che si registri con l'applicazione.

Il motivo delle molteplici visualizzazioni che abbiamo clienti a cui piace inserire forme in modi diversi. La nostra base di clienti è divisa a metà tra coloro che desiderano inserire i parametri di forma in una tabella e quelli che desiderano entrare con una rappresentazione grafica della forma di fronte a loro. Abbiamo anche bisogno di accedere ai parametri a volte attraverso una finestra di dialogo minima piuttosto che la nostra schermata di immissione della forma completa. Da qui le molteplici visualizzazioni.

I comandi che manipolano le forme rientrano in una delle due categorie. O manipolano il percorso di taglio o manipolano i parametri di forma. Per manipolare i parametri della forma in genere, li gettiamo nella schermata di immissione della forma o mostriamo la finestra di dialogo minima. Ricalcolare la forma e visualizzarla nella stessa posizione.

Per il percorso di taglio abbiamo raggruppato ciascuna operazione in un oggetto comando separato. Per esempio abbiamo oggetti comando

ResizePath RotatePath MovePath SplitPath e così via.

Quando abbiamo bisogno di aggiungere nuove funzionalità, aggiungiamo un altro oggetto comando, troviamo un menu, una scorciatoia da tastiera o una barra degli strumenti nella schermata dell'interfaccia utente destra e configuriamo l'oggetto UI per eseguire il comando.

Ad esempio

CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath 

o

CuttingTableScreen.Toolbar("Edit Path").AddButton Application.Icons("MirrorPath"),"Mirror Path", New MirrorPath 

In entrambi i casi i oggetto MirrorPath Command viene associato ad un elemento dell'interfaccia utente desideri. Nel metodo execute di MirrorPath è tutto il codice necessario per riflettere il percorso in un particolare asse. Probabilmente il comando avrà una propria finestra di dialogo o utilizzerà uno degli elementi dell'interfaccia utente per chiedere all'utente quale asse riflettere. Nessuno di questi sta facendo un visitatore, o aggiungendo un metodo al percorso.

Troverete che molto può essere gestito attraverso le operazioni di raggruppamento in comandi. Tuttavia, avverto che non si tratta di una situazione in bianco o nero. Troverete ancora che certe cose funzionano meglio come metodi sull'oggetto originale. Nell'esperienza maggio ho scoperto che forse l'80% di quello che facevo nei metodi era in grado di essere spostato nel comando. L'ultimo 20% funziona semplicemente meglio sull'oggetto.

Ora a qualcuno potrebbe non piacere perché sembra violare gli incapsulamenti. Dal mantenimento del nostro software come sistema orientato agli oggetti nell'ultimo decennio, devo dire che la cosa più importante a lungo termine che puoi fare è documentare chiaramente le interazioni tra i diversi livelli del tuo software e tra i diversi oggetti.

Il raggruppamento di azioni in oggetti di comando aiuta a questo obiettivo molto meglio di una devozione servile agli ideali di incapsulamento.Tutto ciò che è necessario fare per riflettere un percorso è raggruppato nell'oggetto comando Percorso specchio.

+0

La soluzione sembra interessante, ma sarei grato se potessi farmi riferimento al codice di esempio per tale soluzione per capire meglio il concetto. –

2

Indipendentemente dal percorso scelto, l'implementazione della funzionalità alternativa attualmente fornita dal pattern Visitor dovrà "sapere" qualcosa sull'implementazione concreta dell'interfaccia su cui sta lavorando. Quindi non è possibile aggirare il fatto che si dovrà scrivere una funzionalità aggiuntiva "visitatore" per ogni implementazione aggiuntiva. Detto questo, quello che stai cercando è un approccio più flessibile e strutturato per creare questa funzionalità.

È necessario separare la funzionalità visitatore dall'interfaccia della forma.

Quello che vorrei proporre è un approccio creazionista attraverso una fabbrica astratta per creare implementazioni di sostituzione per la funzionalità dei visitatori.

public interface IShape { 
    // .. common shape interfaces 
} 

// 
// This is an interface of a factory product that performs 'work' on the shape. 
// 
public interface IShapeWorker { 
    void process(IShape shape); 
} 

// 
// This is the abstract factory that caters for all implementations of 
// shape. 
// 
public interface IShapeWorkerFactory { 
    IShapeWorker build(IShape shape); 
    ... 
} 

// 
// In order to assemble a correct worker we need to create 
// and implementation of the factory that links the Class of 
// shape to an IShapeWorker implementation. 
// To do this we implement an abstract class that implements IShapeWorkerFactory 
// 
public AbsractWorkerFactory implements IShapeWorkerFactory { 

    protected Hashtable map_ = null; 

    protected AbstractWorkerFactory() { 
      map_ = new Hashtable(); 
      CreateWorkerMappings(); 
    } 

    protected void AddMapping(Class c, IShapeWorker worker) { 
      map_.put(c, worker); 
    } 

    // 
    // Implement this method to add IShape implementations to IShapeWorker 
    // implementations. 
    // 
    protected abstract void CreateWorkerMappings(); 

    public IShapeWorker build(IShape shape) { 
     return (IShapeWorker)map_.get(shape.getClass()) 
    } 
} 

// 
// An implementation that draws circles on graphics 
// 
public GraphicsCircleWorker implements IShapeWorker { 

    Graphics graphics_ = null; 

    public GraphicsCircleWorker(Graphics g) { 
     graphics_ = g; 
    } 

    public void process(IShape s) { 
     Circle circle = (Circle)s; 
     if(circle != null) { 
      // do something with it. 
      graphics_.doSomething(); 
     } 
    } 

} 

// 
// To replace the previous graphics visitor you create 
// a GraphicsWorkderFactory that implements AbstractShapeFactory 
// Adding mappings for those implementations of IShape that you are interested in. 
// 
public class GraphicsWorkerFactory implements AbstractShapeFactory { 

    Graphics graphics_ = null; 
    public GraphicsWorkerFactory(Graphics g) { 
     graphics_ = g; 
    } 

    protected void CreateWorkerMappings() { 
     AddMapping(Circle.class, new GraphicCircleWorker(graphics_)); 
    } 
} 


// 
// Now in your code you could do the following. 
// 
IShapeWorkerFactory factory = SelectAppropriateFactory(); 

// 
// for each IShape in the heirarchy 
// 
for(IShape shape : shapeTreeFlattened) { 
    IShapeWorker worker = factory.build(shape); 
    if(worker != null) 
     worker.process(shape); 
} 

ancora significa che si deve scrivere implementazioni concrete per lavorare su nuove versioni di 'forma', ma perché si è completamente separato dall'interfaccia di forma, è possibile corredare questa soluzione senza rompere l'interfaccia originale e il software che interagisce con esso. Agisce come una sorta di ponteggio attorno alle implementazioni di IShape.

+0

in AbstractWorkerFactory è ancora necessario eseguire l'istanza di –

1

Se si utilizza Java: Sì, si chiama instanceof. Le persone hanno troppa paura di usarlo. Rispetto al modello di visitatore, è generalmente più veloce, più diretto e non afflitto dal punto 5.

+0

più veloce? Controlla [questo] (http://alexshabanov.com/2011/12/03/instanceof-vs-visitor/). – ntohl

+0

@ntohl Nei test che ho fatto (su Java 8, si noti che il test utilizzato Java 6) instanceof era più veloce, quindi suppongo che la velocità della velocità relativa dei due debba variare in base a dettagli sottili. – Andy

1

Se si dispone di n IShape operazioni s e m che si comportano in modo diverso per ogni forma, allora sono necessarie n * m singole funzioni. Mettere tutto questo nella stessa classe mi sembra una pessima idea, dandoti una sorta di oggetto divino. Quindi dovrebbero essere raggruppati per IShape, inserendo m funzioni, una per ogni operazione, nell'interfaccia IShape, o raggruppate per operazione (utilizzando il modello di visitatore), inserendo n funzioni, una per ogni IShape in ogni operazione/visitatore classe.

È necessario aggiornare più classi quando si aggiunge un nuovo IShape o quando si aggiunge una nuova operazione, non c'è modo di aggirarla.


Se siete alla ricerca di ogni operazione per implementare una funzione predefinita IShape, allora sarebbe risolvere il problema, come nella risposta di Daniel Martin: https://stackoverflow.com/a/986034/1969638, anche se mi sarebbe probabilmente usare sovraccarico:

interface IVisitor 
{ 
    void visit(IShape shape); 
    void visit(Rectangle shape); 
    void visit(Circle shape); 
} 

interface IShape 
{ 
    //... 
    void accept(IVisitor visitor); 
} 
3

Il modello di progettazione dei visitatori è una soluzione alternativa, non una soluzione al problema. Risposta breve sarebbe pattern matching.