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

Refactoring advice: How to avoid type checking in this OO design

本文关键字:检查 类型 OO 构建 重构      更新时间:2023-10-16

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

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

代码类似于(为了表示简单,所有代码都是公开的):

// 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()时看到显式类型检查,然后强制转换为派生类型。有哪些选项可以重构它以避免类型检查和类型强制转换?

在我看来,重构的起点应该是这些语句:

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

相同类型的语句重复出现的事实,在我看来,是需要重构它的标志。

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

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

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

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

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

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

另一种方法是在Position中公开一个方法,该方法可以将菜单推送到堆栈上,并在Menu:Execute开始时调用该方法。那么OnEnterPressed的主体就变成了

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

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

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

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

您需要的是表达"操作或菜单"的能力,如果操作和菜单具有非常不同的接口,那么使用多态性编写这一点是非常麻烦的。

不是试图将它们强制进入一个公共接口(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:如果您不使用太旧的编译器,则可以拼接并保留旧的迭代器)]

语言已经提供了这种机制-它是dynamic_cast。但是,从更一般的意义上讲,您的设计的内在缺陷是:

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

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