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?
IMO, the refactoring starting point would be these statements:
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
Executeaction has the meaning: activate the submenu (I am using activate in a generic sense). When the item is not a submenu, thenExecuteis 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.