重构建议:如何避免在此 OO 设计中进行类型检查

发布于 2024-11-28 05:09:32 字数 2138 浏览 4 评论 0原文

我正在寻找有关重构的建议,以改进我的类设计并避免类型检查。

我正在使用命令设计模式来构建菜单树。菜单中的项目可以是各种类型(例如,立即操作[如“保存”]、根据其状态显示复选/图标的切换开/关属性[如“斜体”]等)。至关重要的是,还有子菜单,它们替换屏幕上的当前菜单(而不是显示在旁边)。这些子菜单当然包含自己的菜单项列表,其中可以有更多嵌套的子菜单。

代码类似于(为了简化演示,全部公开):

// Abstract base class
struct MenuItem
{
  virtual ~MenuItem() {}
  virtual void Execute()      = 0;
  virtual bool IsMenu() const = 0;
};

// Concrete classes
struct Action : MenuItem
{
  void Execute() { /*...*/ }
  bool IsMenu() const { return false; }
  // ...
};

// ... other menu items

struct Menu : MenuItem
{
  void Execute() { /* Display menu */ }
  bool IsMenu() const { return true; }
  // ...
  std::vector<MenuItem*> m_items;
  typedef std::vector<MenuItem*>::iterator ItemIter;
};

主菜单只是 Menu 的一个实例,一个单独的类跟踪菜单位置,包括如何进入和退出子菜单:

struct Position
{
  Position( Menu* menu ) 
    : m_menu( menu ) 
  {
    // Save initial position
    m_pos.push_back( MenuPlusIter( m_menu, m_menu->m_items.begin() ) );
  }

  // Ignore error conditions for simplicity
  void OnUpPressed()   { m_pos.back().iter--; }
  void OnDownPressed() { m_pos.back().iter++; }
  void OnBackPressed() { m_pos.pop_back();    }

  void OnEnterPressed()
  {
    MenuItem* item = *m_pos.back().iter;
    // Need to behave differently here if the currently 
    // selected item is a submenu
    if( item->IsMenu() )
    {
      // dynamic_cast not needed since we know the type
      Menu* submenu = static_cast<Menu*>( item );

      // Push new menu and position onto the stack
      m_pos.push_back( MenuPlusIter( submenu, submenu->m_items.begin() ) );

      // Redraw
      submenu->Execute();
    }
    else
    {
      item->Execute();
    }
  }

private:
  struct MenuPlusIter
  {
      Menu*          menu;
      Menu::ItemIter iter;

      MenuPlusIter( Menu* menu_, Menu::ItemIter iter_ )
          : menu( menu_ )
          , iter( iter_ )
      {}
  };

  Menu* m_menu;
  std::vector<MenuPlusIter> m_pos;
};

关键功能是 Position ::OnEnterPressed(),您可以在对 MenuItem::IsMenu() 的调用中看到显式类型检查,然后强制转换为派生类型。有哪些选项可以重构它以避免类型检查和强制转换?

I'm looking for advice on refactoring to improve my class design and avoid type checking.

I am using the Command design pattern to construct a menu tree. An item in the menu could be of various types (e.g., an immediate action [like "Save"], a toggle on/off property which displays with check/icon depending on its state [like "italics"], etc.). Crucially, there are also submenus, which replace (rather than displaying off to the side of) the current menu on the screen. These submenus of course contain their own list of menu items, which could have more nested submenus.

The code is something like (all public for simplicity in presentation):

// Abstract base class
struct MenuItem
{
  virtual ~MenuItem() {}
  virtual void Execute()      = 0;
  virtual bool IsMenu() const = 0;
};

// Concrete classes
struct Action : MenuItem
{
  void Execute() { /*...*/ }
  bool IsMenu() const { return false; }
  // ...
};

// ... other menu items

struct Menu : MenuItem
{
  void Execute() { /* Display menu */ }
  bool IsMenu() const { return true; }
  // ...
  std::vector<MenuItem*> m_items;
  typedef std::vector<MenuItem*>::iterator ItemIter;
};

The main menu is just an instance of Menu, and a separate class keeps track of the menu position, including how to go into and out of submenus:

struct Position
{
  Position( Menu* menu ) 
    : m_menu( menu ) 
  {
    // Save initial position
    m_pos.push_back( MenuPlusIter( m_menu, m_menu->m_items.begin() ) );
  }

  // Ignore error conditions for simplicity
  void OnUpPressed()   { m_pos.back().iter--; }
  void OnDownPressed() { m_pos.back().iter++; }
  void OnBackPressed() { m_pos.pop_back();    }

  void OnEnterPressed()
  {
    MenuItem* item = *m_pos.back().iter;
    // Need to behave differently here if the currently 
    // selected item is a submenu
    if( item->IsMenu() )
    {
      // dynamic_cast not needed since we know the type
      Menu* submenu = static_cast<Menu*>( item );

      // Push new menu and position onto the stack
      m_pos.push_back( MenuPlusIter( submenu, submenu->m_items.begin() ) );

      // Redraw
      submenu->Execute();
    }
    else
    {
      item->Execute();
    }
  }

private:
  struct MenuPlusIter
  {
      Menu*          menu;
      Menu::ItemIter iter;

      MenuPlusIter( Menu* menu_, Menu::ItemIter iter_ )
          : menu( menu_ )
          , iter( iter_ )
      {}
  };

  Menu* m_menu;
  std::vector<MenuPlusIter> m_pos;
};

The key function is Position::OnEnterPressed(), where you see an explicit type check in the call to MenuItem::IsMenu() and then a cast to the derived type. What are some options to refactor this to avoid the type check and cast?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

烟─花易冷 2024-12-05 05:09:32

IMO,重构的起点将是这些语句:

 1. m_pos.push_back( MenuPlusIter( m_menu, m_menu->m_items.begin() ) );

 2. m_pos.push_back( MenuPlusIter( submenu, submenu->m_items.begin() ) );

IMO,相同类型的语句重复本身的事实是需要重构的标志。

如果您可以将 (1) 纳入基类的方法中,然后在派生类中重写它以考虑特定行为 (2),那么您可以将其放入 Execute 中。

如果我错了,请纠正我:这个想法是菜单有项目,每个项目都有一个与其关联的操作,当检测到某些事件时会触发该操作。

现在,当您选择的项目是子菜单时,执行操作的含义是:激活子菜单(我使用的是一般意义上的激活)。当该项目不是子菜单时,执行 是一个不同的野兽。

我没有完全理解您的菜单系统,但在我看来,您有一种层次结构菜单/子菜单(位置),以及根据节点类型触发的一些操作。

我设想菜单/子菜单关系是一个层次结构,允许您定义叶节点(当您没有子菜单时)和非叶节点(子菜单)。叶节点调用一个操作,非叶节点调用另一种处理激活子菜单的操作(此操作返回到菜单系统,因此您不必在其中封装有关菜单系统的知识,您只需将操作转发到菜单系统)。

不知道这对你来说是否有意义。

IMO, the refactoring starting point would be these statements:

 1. m_pos.push_back( MenuPlusIter( m_menu, m_menu->m_items.begin() ) );

 2. m_pos.push_back( MenuPlusIter( submenu, submenu->m_items.begin() ) );

The fact that the same kind of statement repeat itself is, IMO, the sign for the need to refactor that.

If you could factor (1) in a method of your base class, and then override it in the derived class to take into account the specific behavior (2), then you could just put this in Execute.

Correct me if I am wrong: the idea is the a menu has items, and each item has got an action associated to it that is triggered when some event is detected.

Now, when the item you select is a submenu, the the Execute action has the meaning: activate the submenu (I am using activate in a generic sense). When the item is not a submenu, then Execute is a different beast.

I don't have a full comprehension of your menu system, but it seems to me you have a sort of hierarchy menu/submenu (the positions), and some actions that are triggered depending of the kind of node.

What I envision is that the menu/submenu relationship is a hierarchy that allows you to define leaf-nodes (when you don't have a submenu), and non-leaf-nodes (the submenu). A leaf node invoke an action, a non-leaf-node invoke a different kind of action which deals with activating a submenu (this action goes back to the menu system, so you do not encapsulate knowledge about the menu system in it, you simply relay the action to menu system).

Don't know if this makes sense to you.

余厌 2024-12-05 05:09:32

另一种方法是在 Position 中公开一个方法,使 Menu 能够被推入堆栈,并在 Menu:Execute 开始时调用该方法。然后 OnEnterPressed 的主体就变成了

(*m_pos.back().iter)->Execute();

An alternative would be to expose a method in Position which enables a Menu to be pushed onto the stack, and call that method at the start of Menu:Execute. Then the body of OnEnterPressed just becomes

(*m_pos.back().iter)->Execute();
一曲琵琶半遮面シ 2024-12-05 05:09:32

这可能不是您正在寻找的响应,但在我看来,您的解决方案远远优于任何不涉及类型检查的解决方案。

大多数 C++ 程序员对需要检查对象的类型才能决定如何处理它的想法感到不舒服。然而,在 Objective-C 等其他语言和大多数弱类型脚本语言中,这实际上是受到高度鼓励的。

在您的情况下,我认为使用类型检查是很好的选择,因为您需要 Position 功能的类型信息。恕我直言,将此功能移至 MenuItem 子类之一会违反权限分离。 Position 与菜单的查看和控制部分有关。我不明白为什么模型类 MenuMenuItem 应该关心这一点。转向无类型检查解决方案会降低面向对象方面的代码质量。

This probably isn't the response you were looking for, but in my opinion your solution is by far superior to any solution that doesn't involve type checking.

Most C++ programmers are offended by the idea that you need to check an object's type in order to decide what to do with it. Yet in other languages like Objective-C and most weakly typed script languages this is actually highly encouraged.

In your case I think using the type check is well chosen since you need the type information for the functionality of Position. Moving this functionality into one of the MenuItem subclasses would IMHO violate competence separation. Position is concerned with the viewing and controller part of your menu. I don't see why the model classes Menu or MenuItem should be concerned with that. Moving to a no-typecheck solution would decrease code quality in terms of object orientation.

做个ˇ局外人 2024-12-05 05:09:32

您需要的是表达“动作或菜单”的能力,如果动作和菜单具有非常不同的界面,那么使用多态性编写就非常麻烦。

我不会试图强制它们进入一个公共接口(Execute 对于子菜单方法来说是一个糟糕的名称),我会比你走得更远,使用 dynamic_cast

此外,dynamic_cast总是优于标志和static_cast。操作不需要告诉世界它们不是子菜单。

用最惯用的 C++ 重写,给出以下代码。我使用 std::list 是因为它的便捷方法 spliceinsertremove 不会使迭代器失效(使用链表的几个充分理由之一)。我还使用 std::stack 来跟踪打开的菜单。

struct menu_item
{
    virtual ~menu_item() {}

    virtual std::string label() = 0;
    ...
};

struct action : menu_item
{
    virtual void execute() = 0;
};

struct submenu : menu_item
{
    // You should go virtual here, and get rid of the items member.
    // This enables dynamically generated menus, and nothing prevents
    // you from having a single static_submenu class which holds a 
    // vector or a list of menu_items.
    virtual std::list<menu_item*> unfold() = 0;
};

struct menu
{
    void on_up() { if (current_item != items.begin()) current_item--; }
    void on_down() { if (++current_item == items.end()) current_item--; }

    void on_enter()
    {
        if (submenu* m = dynamic_cast<submenu*>(current_item))
        {
            std::list<menu_item*> new_menu = m->unfold();

            submenu_stack.push(submenu_info(*current_item, new_menu));

            items.splice(current_item, new_menu);
            items.erase(current_item);
            current_item = submenu_stack.top().begin;

            redraw(current_item, items.end());
        }

        else if (action* a = dynamic_cast<action*>(current_item))
            a->execute();

        else throw std::logic_error("Unknown menu entry type");

        // If we were to add more else if (dynamic_cast) clauses, this
        // would mean we don't have the right design. Here we are pretty
        // sure this won't happen. This is what you say to coding standard
        // nazis who loathe RTTI.
    }

    void on_back()
    {
        if (!submenu_stack.empty())
        {
            const submenu_info& s = submenu_stack.top();

            current_item = items.insert(items.erase(s.begin, s.end), s.root);
            submenu_stack.pop();

            redraw(current_item, items.end());
        }
    }

    void redraw(std::list<menu_item*>::iterator begin, 
                std::list<menu_item*>::iterator end)
    {
       ...
    }

private:
    std::list<menu_item*> items;
    std::list<menu_item*>::iterator current_item;

    struct submenu_info
    {
        submenu* root;
        std::list<menu_item*>::iterator begin, end;

        submenu_info(submenu* root, std::list<menu_item*>& m)
            : root(root), begin(m.begin()), end(m.end())
        {}
    };

    std::stack<submenu_info> submenu_stack;
};

我试图让代码简单明了。如果有不清楚的地方,请随时询问。

[关于执行 splice 时的迭代器失效,请参阅此问题(tl;dr:如果您不使用太旧的编译器,则可以拼接并保留旧的迭代器)。]

What you need is the ability to express "either an action or a menu", which is very cumbersome to write using polymorphism if actions and menus have very different interfaces.

Instead of trying to force them into a common interface (Execute is a poor name for the submenu method), I'd go further than you and use dynamic_cast.

Also, dynamic_cast is always superior to a flag and static_cast. Actions do not neet to tell the world that they aren't submenus.

Rewritten in the most idiomatic C++, this gives the following code. I use std::list because of its convenience methods splice, insert and remove which don't invalidate iterators (one of the few good reasons for using linked lists). I also use std::stack for keeping track of the open menus.

struct menu_item
{
    virtual ~menu_item() {}

    virtual std::string label() = 0;
    ...
};

struct action : menu_item
{
    virtual void execute() = 0;
};

struct submenu : menu_item
{
    // You should go virtual here, and get rid of the items member.
    // This enables dynamically generated menus, and nothing prevents
    // you from having a single static_submenu class which holds a 
    // vector or a list of menu_items.
    virtual std::list<menu_item*> unfold() = 0;
};

struct menu
{
    void on_up() { if (current_item != items.begin()) current_item--; }
    void on_down() { if (++current_item == items.end()) current_item--; }

    void on_enter()
    {
        if (submenu* m = dynamic_cast<submenu*>(current_item))
        {
            std::list<menu_item*> new_menu = m->unfold();

            submenu_stack.push(submenu_info(*current_item, new_menu));

            items.splice(current_item, new_menu);
            items.erase(current_item);
            current_item = submenu_stack.top().begin;

            redraw(current_item, items.end());
        }

        else if (action* a = dynamic_cast<action*>(current_item))
            a->execute();

        else throw std::logic_error("Unknown menu entry type");

        // If we were to add more else if (dynamic_cast) clauses, this
        // would mean we don't have the right design. Here we are pretty
        // sure this won't happen. This is what you say to coding standard
        // nazis who loathe RTTI.
    }

    void on_back()
    {
        if (!submenu_stack.empty())
        {
            const submenu_info& s = submenu_stack.top();

            current_item = items.insert(items.erase(s.begin, s.end), s.root);
            submenu_stack.pop();

            redraw(current_item, items.end());
        }
    }

    void redraw(std::list<menu_item*>::iterator begin, 
                std::list<menu_item*>::iterator end)
    {
       ...
    }

private:
    std::list<menu_item*> items;
    std::list<menu_item*>::iterator current_item;

    struct submenu_info
    {
        submenu* root;
        std::list<menu_item*>::iterator begin, end;

        submenu_info(submenu* root, std::list<menu_item*>& m)
            : root(root), begin(m.begin()), end(m.end())
        {}
    };

    std::stack<submenu_info> submenu_stack;
};

I tried to keep the code straightforward. Feel free to ask if something is unclear.

[About iterator invalidation when doing splice, see this question (tl;dr: it is OK to splice and keep the old iterators provided you don't use a too old compiler).]

眉黛浅 2024-12-05 05:09:32

该语言已经提供了这种机制 - 它是dynamic_cast。然而,从更一般的意义上来说,您的设计中固有的缺陷是:

m_pos.push_back( MenuPlusIter( submenu, submenu->m_items.begin() ) );

它应该放在 Execute() 函数中,并根据需要进行重构以实现这一点。

The language already provides this mechanism- it's dynamic_cast. However, in a more general sense, the inherent flaw in your design is this:

m_pos.push_back( MenuPlusIter( submenu, submenu->m_items.begin() ) );

It should go in the Execute() function, and refactor as necessary to make that happen.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文