2010-07-25 11 views
6

Ho un'interfaccia utente con una vista ad albero sulla sinistra e un visualizzatore sulla destra (un po 'come un client di posta elettronica). Il visualizzatore sulla destra mostra i dettagli di tutto ciò che ho selezionato nell'albero a sinistra.Il modo migliore per implementare azioni performanti su nodi ad albero, preferibilmente senza usare visitatori

L'interfaccia utente dispone di pulsanti "aggiungi", "modifica" e "elimina". Questi pulsanti agiscono in modo diverso a seconda del "nodo" nell'albero selezionato.

Se si seleziona un nodo di un particolare tipo e l'utente fa clic su "modifica", è necessario aprire la finestra di dialogo di modifica appropriata per quel particolare tipo di nodo, con i dettagli di tale nodo.

Ora, ci sono molti tipi diversi di nodi e l'implementazione di una classe di visitatori è un po 'caotica (attualmente il mio visitatore ha circa 48 voci ...). Funziona bene però - fondamentalmente per la modifica di avere qualcosa come una classe OpenEditDialog che eredita il visitatore e apre la finestra di modifica appropriata:

abstractTreeNode-> accept (OpenEditDialog());

Il problema è che devo implementare la classe di visitatori astratti per ogni "azione" che voglio eseguire sul nodo e per qualche motivo non posso fare a meno di pensare che mi manca un trucco.

L'altro modo potrebbe essere stata per implementare le funzioni nei nodi stessi:

abstractTreeNode->openEditDialog(); 

che sto ording il nodo intorno un po 'qui, quindi forse questo è meglio:

abstractTreeNode->editClickedEvent(); 

Non posso fare a meno di pensare che questo sta inquinando il nodo però.

Ho pensato a un terzo modo che non ho ancora pensato tanto. Potrei avere una classe wrapper basata su modelli che viene aggiunta all'albero che consente di chiamare le funzioni libere per eseguire qualsiasi azione, quindi suppongo che agisca da interfaccia tra i nodi e l'interfaccia:

(codice pseudo la parte superiore della mia testa solo per dare un'idea):

template <class T> 
TreeNode(T &modelNode) 
{ 
    m_modelNode = modelNode; 
} 

template <> 
void TreeNode<AreaNode>::editClickedEvent() 
{ 
    openEditDialog(m_modelNode); // Called with concrete AreaNode 
} 

template <> 
void TreeNode<LocationNode>::editClickedEvent() 
{ 
    openEditDialog(m_modelNode); // Called with concrete LocationNode 
} 

ecc ..

Quindi questo è efficacemente estendendo i nodi, ma in un modo diverso di utilizzare il visitatore e sembra un po 'più ordinato .

Ora prima di andare avanti e fare il grande passo con uno di questi metodi, ho pensato che sarebbe saggio ottenere un input.

Grazie! Spero che tutto questo fa un certo senso ..

EDIT:

ho preso in giro l'idea involucro su modelli ..

class INode 
{ 
public: 
    virtual ~INode() {} 
    virtual void foo() = 0; 
}; 

class AreaNode : public INode 
{ 
public: 
    AreaNode() {} 
    virtual ~AreaNode() {} 
    void foo() { printf("AreaNode::foo\r\n"); } 
}; 

class RoleNode : public INode 
{ 
public: 
    RoleNode() {} 
    virtual ~RoleNode() {} 
    void foo() { printf("RoleNode::foo\r\n"); } 
}; 

class ITreeNode 
{ 
public: 
    virtual ~ITreeNode() {} 
    virtual void bar() = 0; 
    virtual void foo() = 0; 
}; 

template <class T> 
class MainViewTreeNode : public ITreeNode 
{ 
public: 
    MainViewTreeNode() : m_node() {} 
    virtual ~MainViewTreeNode() {} 
    void bar() {} 
    void foo() { m_node.foo(); } 
protected: 
    T m_node; 
}; 

template <> 
void MainViewTreeNode<AreaNode>::bar() 
{ 
    printf("MainViewTreeNode<AreaNode>::bar\r\n"); 
} 

template <> 
void MainViewTreeNode<RoleNode>::bar() 
{ 
    printf("MainViewTreeNode<RoleNode>::bar\r\n"); 
} 

int _tmain(int argc, _TCHAR* argv[]) 
{ 
    MainViewTreeNode<RoleNode> role; 
    MainViewTreeNode<AreaNode> area; 

    std::list<ITreeNode*> nodes; 
    nodes.push_back(&role); 
    nodes.push_back(&area); 

    std::list<ITreeNode*>::iterator it = nodes.begin(); 

    for (; it != nodes.end(); ++it) 
    { 
     (*it)->foo(); 
     (*it)->bar(); 
    } 

    getchar(); 
    return 0; 
} 

Grazie.

risposta

2

visitatore è utile quando si hanno molte operazioni e alcuni tipi.Se hai molti tipi, ma poche operazioni, usa il normale polimorfismo.

+0

Anche con poche operazioni, il pattern Visitor potrebbe ancora migliorare il disaccoppiamento e la modularità del codice. – Thomas

+0

solo per essere chiari, vuoi dire forse in questo caso solo implementare le funzioni direttamente nelle classi del nodo? – Mark

+0

@marksim: Sì, questo è quello che intendevo. OTOH, @Thomas ha un punto: si tratta di operazioni intrecciate sull'albero con i dati dell'albero.Mettere dati e operazioni in oggetti è ciò che è OOP, ma non penso che OOP sia il Santo Graal. Ha i suoi svantaggi. Alla fine, sei l'unico di noi che conosce abbastanza il dominio dell'applicazione per essere in grado di prendere una decisione ben fondata. Possiamo solo suggerire possibili soluzioni. – sbi

1

Tali problemi sono, sfortunatamente, fin troppo comuni con C++ e con le lingue OO tipizzate staticamente. Recentemente sono incappato in this article che descrive come implementare il doppio invio con una tabella di ricerca personalizzata.

Posso vedere un approccio simile a lavorare qui. In sostanza, si crea una tabella di involucri funzione del tipo Entry di seguito:

class EntryBase { 
    public: 
     virtual bool matches(TreeNode const &node) const = 0; 
     virtual void operator()(TreeNode &node) const = 0; 
}; 

template<typename NodeType, typename Functor> 
class Entry : public EntryBase { 
    Functor d_func; 
    public: 
     Entry(Functor func) : d_func(func) { } 
     virtual bool matches(TreeNode const &node) const { 
      return dynamic_cast<NodeType const *>(&node) != 0; 
     } 
     virtual void operator()(TreeNode &node) const { 
      d_func(dynamic_cast<NodeType &>(node)); 
     } 
}; 

Ciascun tavolo rappresenterà quindi un tipo di visitatore (che si può fare questo senza Boost troppo, naturalmente):

class NodeVisitor { 
    typedef boost::shared_ptr<EntryBase> EntryPtr; 
    typedef std::vector<EntryPtr> Table; 
    Table d_entries; 
    public: 
     template<typename NodeType, typename Functor> 
     void addEntry(Functor func) { 
      EntryPtr entry(new Entry<NodeType, Functor>(func)); 
      d_entries.push_back(entry); 
     } 
     void visit(TreeNode &node) { 
      EntryPtr entry = lookup(node); 
      if (!entry) 
       return; // this Visitor doesn't handle this type 
      (*entry)(node); 
     } 
    private: 
     EntryPtr lookup(TreeNode &node) { 
      Table::const_iterator iter = 
       std::find_if(d_entries.begin(), d_entries.end(), 
          boost::bind(&EntryBase::matches, _1, boost::ref(node))); 
      if (iter != d_entries.end()) 
       return *iter; 
      return 0; 
     } 
}; 

Costruzione di una tabella sarebbe qualcosa di simile:

void addToCompany(CompanyNode &company) { ... } 
void addToEmployee(EmployeeNode &employee) { ... } 

NodeVisitor nodeAdder; 
nodeAdder.addEntry<CompanyNode>(&addToCompany); 
nodeAdder.addEntry<EmployeeNode>(&addToEmployee); 

Dopo tutto questo lavoro, si può semplicemente scrivere (senza aggiunte di TreeNode o r qualsiasi classe che eredita da TreeNode):

nodeAdder.visit(someNode); 

I modelli assicurano che il dynamic_cast riesce sempre, quindi è abbastanza sicuro. Il più grande svantaggio è, naturalmente, che non è il più veloce del mondo. Ma per aprire una finestra di dialogo, l'utente è probabilmente il fattore più lento, quindi dovrebbe essere abbastanza veloce.

Ho appena implementato questo visitatore nel mio progetto e funziona come un incantesimo!

+0

@Thomas: Sembra interessante, penso che avrò bisogno di studiarlo un po 'per capirlo. Nel frattempo, cosa ne pensi della modifica al mio post iniziale sopra? – Mark

+0

Avrò bisogno di studiare anche quello ... ma si sta facendo tardi e sono troppo stanco per quello. Proverò a ricordare di dare un'occhiata domani. O forse nel frattempo arriverà qualcuno più esperto. – Thomas

+0

@Thomas: Grazie, sì si sta facendo un po 'tardi per tutto questo! Apprezzo molto l'input. – Mark

1

Invece di utilizzare m_node.foo(), ciò che si dovrebbe fare è l'ereditarietà statica. Questa è fondamentalmente la tua idea di "template wrapper", ma è un modello ben definito.

class ITreeNode 
{ 
public: 
    virtual ~ITreeNode() {} 
    virtual void bar() = 0; 
    virtual void foo() = 0; 
}; 

template <class T> 
class MainViewTreeNode : public ITreeNode 
{ 
public: 
    MainViewTreeNode() : m_node() {} 
    virtual ~MainViewTreeNode() {} 
    void bar() {} 
    void foo() { m_node.foo(); } 
protected: 
    T m_node; 
}; 

diventa

class ITreeNode 
{ 
public: 
    virtual ~ITreeNode() {} 
    virtual void bar() = 0; 
    virtual void foo() = 0; 
}; 

template <class T> 
class MainViewTreeNode : public ITreeNode 
{ 
public: 
    MainViewTreeNode() {} 
    virtual ~MainViewTreeNode() {} 
    void bar() { T::bar(); } 
    void foo() { T::foo(); } 
}; 
class RoleNode : public MainViewTreeNode<RoleNode> { 
    void bar() { std::cout << "Oh hai from RoleNode::bar()! \n"; } 
    void foo() { std::cout << "Oh hai from RoleNode::foo()! \n"; } 
}; 

Naturalmente, se si dispone già di successione regolare nel mix, perché non utilizzare questo? Non ci sarà alcuna soluzione più semplice del normale polimorfismo qui. Funziona bene quando il numero di tipi è elevato e il numero di operazioni è basso. Forse il difetto nel tuo design è quanti tipi hai.

+0

Il problema è (anche se perdonami se mi manca qualcosa), quel codice rende le classi dei nodi dipendenti da MainViewTreeNode, e ora l'implementazione di foo e bar si trovano nella classe nodo. Quello che sto cercando di fare è mantenere il nodo stesso dall'implementazione di "bar" senza utilizzare il pattern visitor. Tuttavia, vedo quello che hai fatto ed è ancora più spunti di riflessione, inoltre prendo in considerazione il tuo altro commento sull'uso del polimorfismo diretto e non mi preoccupo di disaccoppiare così tanto. Grazie :-) – Mark

1

Un altro modello da considerare qui è il Command pattern. Fai in modo che i tuoi nodi memorizzino un elenco di comandi che hanno tutti i metodi GetName & Execute. Quando si seleziona un nodo, si enumera la raccolta e si chiama GetName su ciascun comando per ottenere il nome delle voci di menu e quando si fa clic su una voce di menu si chiama Execute. Questo ti dà la massima flessibilità, puoi impostare i comandi quando viene creato l'albero o nel costruttore del tipo di ogni nodo. In entrambi i casi è possibile riutilizzare i comandi di tutti i tipi e avere un numero variabile di comandi per ciascun tipo.

In generale, tuttavia, la mia esperienza suggerirebbe che sia questo che il modello di visitatore sono probabilmente eccessivi in ​​questo caso e semplicemente il modo di aggiungere metodi virtuali Aggiungi, Modifica ed Elimina sul tipo di nodo dell'albero di base è la strada da percorrere.

+0

grazie, qualche buon cibo per il pensiero lì. Sono decisamente più incline a usare semplicemente aggiungi/modifica/cancella/seleziona direttamente sui nodi, o almeno su una classe template che contiene il nodo concreto simile al mio esempio alla fine del mio post. Nessuno ha detto che sembra orribile, tuttavia ;-) – Mark

Problemi correlati