2010-07-25 8 views
6

Tengo una interfaz de usuario con una vista de árbol a la izquierda y un visor a la derecha (un poco como un cliente de correo electrónico). El visor de la derecha muestra el detalle de lo que he seleccionado en el árbol de la izquierda.La mejor manera de implementar acciones en los nodos de árboles, preferiblemente sin usar visitantes

La interfaz de usuario tiene los botones "agregar", "editar" y "eliminar". Estos botones actúan de manera diferente dependiendo de qué "nodo" en el árbol esté seleccionado.

Si selecciono un nodo de un tipo particular, y el usuario hace clic en "editar", entonces necesito abrir el cuadro de diálogo de edición correspondiente para ese tipo particular de nodo, con los detalles de ese nodo.

Ahora, hay muchos tipos diferentes de nodos y la implementación de una clase de visitante se siente un poco desordenada (actualmente mi visitante tiene alrededor de 48 entradas ...). Aunque funciona bien, básicamente para editar, tener algo así como una clase OpenEditDialog que hereda al visitante, y abre el diálogo de edición apropiado:

abstractTreeNode-> accept (OpenEditDialog());

El problema es que tengo que implementar la clase de visitante abstracta para cada "acción" que deseo realizar en el nodo y por alguna razón no puedo evitar pensar que me estoy perdiendo un truco.

La otra manera podría haber sido para implementar las funciones de los nodos a sí mismos:

abstractTreeNode->openEditDialog(); 

estoy ording el nodo en torno a un poco aquí, así que quizás es mejor:

abstractTreeNode->editClickedEvent(); 

No puedo evitar pensar que esto está contaminando el nodo.

Pensé en una tercera forma en la que todavía no había pensado tanto. Podría tener una clase de contenedor de plantillas que se agregue al árbol, lo que me permite quizás llamar a funciones libres para realizar cualquier acción, así que supongo que actúa como un intermediario entre nodos e interfaz:

(pseudocódigo) de la parte superior de mi cabeza sólo para dar una 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 
} 

etc ..

Así que esto está extendiendo de manera efectiva los nodos, pero de una manera diferente a la utilización de los visitantes y parece un poco más ordenado bits .

Ahora, antes de seguir adelante y dar el paso utilizando uno de estos métodos, pensé que sería conveniente obtener algo de información.

Gracias! Espero que todo esto tiene algún sentido ..

EDIT:

he burlado de la idea de plantilla envoltorio ..

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

Gracias.

Respuesta

2

visitante es útil cuando se tiene muchas operaciones y pocos tipos.Si tiene muchos tipos, pero pocas operaciones, use polimorfismo normal.

+0

Incluso con algunas operaciones, el patrón de visitante aún podría mejorar el desacoplamiento y la modularidad del código. – Thomas

+0

para que quede claro, ¿quiere decir que quizás en este caso simplemente implemente las funciones directamente en las clases de nodo? – Mark

+0

@marksim: Sí, eso es lo que quise decir. OTOH, @Thomas tiene un punto: está entrelazando operaciones en el árbol con los datos del árbol.Poner datos y operaciones en objetos es de lo que se trata OOP, pero no creo que OOP sea el santo grial. Tiene sus desventajas. Al final, usted es el único de nosotros que conoce lo suficiente del dominio de la aplicación para poder tomar una decisión fundada. Solo podemos insinuar posibles soluciones. – sbi

1

Desafortunadamente, estos problemas son muy comunes con C++ y con lenguajes de OO estáticos en general. Recientemente tropecé con this article que describe cómo implementar el despacho doble con una tabla de búsqueda personalizada.

Aquí puedo ver un enfoque similar. Básicamente, se construye una tabla de envoltorios función del tipo Entry a continuación:

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

Cada tabla de este tipo representaría entonces un tipo de visitante (se puede hacer esto sin Boost también, por supuesto):

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

Construcción de una mesa sería algo como esto:

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

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

Después de todo ese trabajo, simplemente puede escribir (sin adiciones a TreeNode o r cualquier clase que hereda de TreeNode):

nodeAdder.visit(someNode); 

Las plantillas aseguran que el dynamic_cast siempre tiene éxito, por lo que es bastante seguro. El mayor inconveniente es, por supuesto, que no es el más rápido del mundo. Pero para abrir un diálogo, el usuario es probablemente el factor más lento, por lo que debe ser bastante rápido.

Acabo de implementar este visitante en mi propio proyecto, y está funcionando como un encanto!

+0

@Thomas: Parece interesante, creo que tendré que estudiarlo un poco para entenderlo. Mientras tanto, ¿qué piensas de la edición de mi publicación inicial anterior? – Mark

+0

Tendré que estudiar eso también ... pero se está haciendo tarde y estoy demasiado cansado para eso. Trataré de recordar echarle un vistazo mañana. O tal vez alguien con más experiencia vendrá mientras tanto. – Thomas

+0

@Thomas: ¡Gracias, sí, se está haciendo un poco tarde para todo esto! Aprecio mucho la entrada. – Mark

1

En lugar de usar m_node.foo(), lo que debe hacer es la herencia estática. Esta es básicamente su idea de "envoltura de plantilla", pero es un patrón bien establecido.

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

convierte

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

Por supuesto, si usted ya tiene la herencia regular en la mezcla, ¿por qué no usar eso? No va a haber una solución más fácil que el polimorfismo normal aquí. Funciona bien cuando la cantidad de tipos es alta y el número de operaciones es bajo. Tal vez el defecto en su diseño es la cantidad de tipos que tiene.

+0

El problema es (aunque perdónenme si me falta algo), ese código hace que las clases de nodos dependan de MainViewTreeNode, y ahora la implementación tanto de foo como de la barra están en la clase de nodo. Lo que intento hacer es evitar que el nodo implemente "barra" sin usar el patrón de visitante. Sin embargo, sí veo lo que has hecho y es aún más algo para pensar, además de tener en cuenta tu otro comentario sobre el uso de polimorfismo directo y no preocuparte por el desacoplamiento tanto. Gracias :-) – Mark

1

Otro patrón a considerar aquí es el Command pattern. Hace que sus nodos almacenen una lista de comandos que tienen todos GetName & Ejecutar métodos. Cuando se selecciona un nodo, enumera la colección y llama a GetName en cada comando para obtener el nombre de los elementos del menú y cuando se hace clic en un elemento del menú, se llama a Execute. Esto le brinda la máxima flexibilidad, puede configurar los comandos cuando se crea el árbol o en el constructor de cada tipo de nodo. De cualquier forma, puede reutilizar comandos entre tipos y tener diferentes números de comandos para cada tipo.

En general, mi experiencia sugiere que tanto este como el patrón de visitante probablemente sean exagerados en este caso y simplemente poner métodos virtuales Agregar, Editar y Eliminar en el tipo de nodo de árbol base es el camino a seguir.

+0

gracias, algo de buena comida para pensar allí. Definitivamente me inclino más hacia el simple uso de agregar/editar/eliminar/seleccionar directamente en los nodos, o al menos en una clase de plantilla que contenga el nodo concreto similar a mi ejemplo al final de mi publicación. Nadie ha dicho que se ve horrible, aún ;-) – Mark

Cuestiones relacionadas