【问题标题】:Refactoring advice: How to avoid type checking in this OO design重构建议:如何避免在这个 OO 设计中进行类型检查
【发布时间】:2011-10-20 04:26:10
【问题描述】:

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

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

代码类似于(为简单起见,全部公开):

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

【问题讨论】:

  • 我看不出演员阵容有什么问题。实际上,我没有看到一种聪明的方法来删除它而不会弄乱代码。毕竟,您确实希望在遇到子菜单时发生一些不同的事情,不是吗?见鬼,我什至会使用 dynamic_cast 并删除这个 IsMenu 方法。

标签: c++ object refactoring type-conversion dynamic-cast


【解决方案1】:

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

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

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

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

我的设想是菜单/子菜单关系是一个层次结构,允许您定义叶节点(当您没有子菜单时)和非叶节点(子菜单)。叶节点调用一个动作,一个非叶节点调用另一种处理激活子菜单的动作(这个动作回到菜单系统,所以你不需要在其中封装有关菜单系统的知识,你只需将动作传递给菜单系统)。

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

【讨论】:

  • 在你的最后一段中,你的意思是向 MenuItem 基类添加类似 void UpdateMenu(vector&) 的东西,除了一个派生类(菜单)之外什么都不做?如果是这样,在我看来,它通过将菜单导航逻辑从指针式 Position 类移到指针类本身来减少“单一责任”原则。除了一个类之外,所有类的空白覆盖也似乎是对继承层次结构的另一种“滥用”,而我将在当前实现中被指控或使用 dynamic_cast 的继承层次结构。
  • 您的更新是有意义的。它似乎仍然在两个非理想选项之间进行选择,所以我必须更多地考虑它。感谢您的意见!
  • 不客气!你知道,很多时候没有比完美解决方案更完美的事情......
【解决方案2】:

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

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

【讨论】:

  • 这似乎在类之间增加了更紧密的(双向)耦合,并且由于该方法对于除 Menu 之外的所有具体 MenuItem 都是空的,因此这似乎只是对继承层次结构的另一种滥用.有什么理由比我更喜欢你的虐待吗?
  • 我不是在推荐MenuItem中的新方法,而是Position中的新方法。诚然,这意味着 MenuItem 现在必须有权访问 Position。我只是建议它作为替代方案,而不是声称它更好或更坏。
【解决方案3】:

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

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

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

【讨论】:

  • 我同意。我会更进一步,并提倡在这里使用dynamic_cast,并摆脱IsMenu。但是,请准备好在代码审查期间保护它免受编码标准纳粹的攻击。
  • 我经常看到这样的说法,即 dynamic_cast(或等效项,如这里)表明设计有缺陷,我认为大多数时候这是正确的。但是在这里,我很难想出一个更令人满意的解决方案。
  • @mlimber:在实践中你会发现dynamic_cast 是可以的,如果你有一个(或者两个)需要特殊处理的子层次结构。在所有情况下,它总是比标志和static_cast 更好。 OO 范式并不总是最好的。在函数式语言中,模式匹配是司空见惯的,并且直接转写在 OOP 中,它给出了向下转换、多次分派和丑陋的访问者模式。我绝对更喜欢抛出一两个放置良好的动态转换,而不是设置访问者模式(当然,如果不存在 更简单 解决方案)。
【解决方案4】:

您需要的是表达“动作或菜单”的能力,如果动作和菜单具有非常不同的接口,使用多态来编写非常麻烦。

与其试图强迫它们进入一个通用界面(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 时的迭代器失效,请参阅this question(tl;dr:如果您不使用太旧的编译器,可以拼接并保留旧的迭代器)。] em>

【讨论】:

  • 感谢您的详细想法!这让我深思。
【解决方案5】:

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

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

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

【讨论】:

  • 重复类似的答案:您的意思是向 MenuItem 基类添加类似 void UpdateMenu(vector&) 的东西,除了一个派生类(菜单)之外什么都不做?如果是这样,在我看来,它通过将菜单导航逻辑从指针式 Position 类移到指针类本身来减少“单一责任”原则。除了一个类之外,所有类的空白覆盖也似乎是对继承层次结构的另一种“滥用”,而我将在当前实现中被指控或使用 dynamic_cast 的继承层次结构。
猜你喜欢
  • 1970-01-01
  • 2019-11-22
  • 1970-01-01
  • 2011-12-15
  • 1970-01-01
  • 2023-01-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多