Лучший способ реализовать выполнение действий на узлах дерева, желательно без использования посетителей - PullRequest
6 голосов
/ 25 июля 2010

У меня есть пользовательский интерфейс с древовидным представлением слева и средством просмотра справа (немного похоже на почтовый клиент).Вьюер справа отображает детали того, что я выбрал в дереве слева.

В пользовательском интерфейсе есть кнопки «добавить», «изменить» и «удалить».Эти кнопки действуют по-разному в зависимости от того, какой «узел» в дереве выбран.

Если у меня выбран узел определенного типа, а пользователь нажимает «изменить», то мне нужно открыть соответствующий диалог редактирования.для этого конкретного типа узла, с деталями этого узла.

Теперь, есть много разных типов узлов, и реализация класса посетителя кажется немного грязной (в настоящее время мой посетитель имеет около 48 записей ....).Это работает хорошо, хотя - в основном для редактирования есть что-то вроде класса OpenEditDialog, который наследует посетителя, и открывает соответствующий диалог редактирования:

abstractTreeNode-> accept (OpenEditDialog ());

Проблема в том, что мне нужно реализовать абстрактный класс посетителя для каждого «действия», которое я хочу выполнить на узле, и по какой-то причине я не могу не думать, что мне не хватает трюка.

Другой способ могЧтобы реализовать функции в самих узлах:

abstractTreeNode->openEditDialog();

Здесь я расскажу немного об узле, так что, может быть, это лучше:

abstractTreeNode->editClickedEvent();

Я не могупомогите, но подумайте, что это загрязняет узел.

Я действительно думал о третьем способе, о котором я еще не думал так много.Вместо этого у меня может быть шаблонный класс-обертка, который добавляется в дерево, что позволяет мне, возможно, вызывать free-функции для выполнения любых действий, поэтому я предполагаю, что он действует как переход между узлами и интерфейсом:

(псевдокод на макушке, просто чтобы дать представление):

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
}

и т. д.

Таким образом, это эффективно расширяет узлы, но отличается от использования посетителя иэто кажется немного более аккуратным.

Теперь, прежде чем я продолжу и сделаю решительный шаг, используя один из этих методов, я подумал, что было бы разумно получить некоторую информацию.

Спасибо!Я надеюсь, что все это имеет некоторый смысл ..

РЕДАКТИРОВАТЬ:

Я смоделировал идею шаблонной оболочки ..

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

Спасибо.

Ответы [ 4 ]

2 голосов
/ 25 июля 2010

Посетитель полезен, когда у вас много операций и несколько типов Если у вас много типов, но мало операций, используйте обычный полиморфизм.

1 голос
/ 26 июля 2010

Другой шаблон, который следует рассмотреть здесь, это Шаблон команды . Вы заставляете свои узлы хранить список команд, у всех из которых есть методы GetName и Execute. Когда узел выбран, вы перечисляете коллекцию и вызываете GetName для каждой команды, чтобы получить имя пунктов меню, а когда вы щелкаете по пункту меню, вы вызываете Execute. Это дает вам максимальную гибкость, вы можете настроить команды при создании дерева или в конструкторе каждого типа узла. В любом случае вы можете повторно использовать команды разных типов и иметь разное количество команд для каждого типа.

Как правило, мой опыт подсказывает, что и этот шаблон, и шаблон посетителя в этом случае, вероятно, излишни, и простое использование виртуальных методов Add, Edit и Delete для типа узла базового дерева - путь.

1 голос
/ 26 июля 2010

Вместо того, чтобы использовать m_node.foo (), вам нужно статическое наследование. По сути, это ваша идея «шаблона-оболочки», но это устоявшийся шаблон.

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

становится

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

Конечно, если у вас уже есть регулярное наследование в миксе, почему бы просто не использовать это? Здесь не будет более простого решения, чем обычный полиморфизм. Это хорошо работает, когда число типов велико, а количество операций мало. Возможно, недостаток вашего дизайна в том, сколько у вас типов.

1 голос
/ 25 июля 2010

Такие проблемы, к сожалению, слишком часто встречаются в C ++ и статически типизированных ОО-языках в целом. Недавно я наткнулся на эту статью , в которой описывается, как реализовать двойную диспетчеризацию с помощью пользовательской таблицы поиска.

Я вижу похожий подход, работающий здесь. По сути, вы строите таблицу оберток функций типа Entry ниже:

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

Тогда каждая такая таблица будет представлять один тип посетителя (конечно, вы можете сделать это и без Boost):

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

Конструкция стола будет выглядеть примерно так:

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

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

После всей этой работы вы могли бы просто написать (без каких-либо дополнений TreeNode или любой класс, который наследуется от TreeNode):

nodeAdder.visit(someNode);

Шаблоны гарантируют, что dynamic_cast всегда будет успешным, так что это довольно безопасно. Самый большой недостаток, конечно, в том, что он не самый быстрый в мире. Но для открытия диалога пользователь, вероятно, является более медленным фактором, поэтому он должен быть достаточно быстрым.

Я только что реализовал этого посетителя в своем собственном проекте, и он работает как шарм!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...