Using the visitor design pattern in arduino-menusystem

Rendering a menu in arduino-menusystem version 2.1.1 isn’t dictated by the API. For example, in the current_menu.ino example the menu is rendered in loop():

 1void loop()
 3  Serial.println("");
 5  // Display the menu
 6  Menu const* cp_menu = ms.get_current_menu();
 7  MenuComponent const* cp_menu_sel = cp_menu->get_selected();
 8  for (int i = 0; i < cp_menu->get_num_menu_components(); ++i)
 9  {
10    MenuComponent const* cp_m_comp = cp_menu->get_menu_component(i);
11    Serial.print(cp_m_comp->get_name());
13    if (cp_menu_sel == cp_m_comp)
14      Serial.print("<<< ");
16    Serial.println("");
17  }
19  // Code ommitted

There are two problems with this approach:

  1. Complex decision blocks would be needed to render each MenuComponent type differently, resulting in code that’s difficult to read and maintain;
  2. It’s not obvious how the menu system should be rendered; the API provides no guideance.

At first glance a reasonable solution would be to add a pure virtual render method to the MenuComponent and override it in each subclass. This provides a clear interface to rendering and removes complex decision blocks for each type.

 1class MenuItem : public MenuComponent {
 3    void render() const {
 4        Serial.println(_name);
 5    }
 8void loop() {
 9  Menu const* cp_menu = ms.get_current_menu();
10  MenuComponent const* cp_menu_sel = cp_menu->get_selected();
11  for (int i = 0; i < cp_menu->get_num_menu_components(); ++i)
12  {
13    MenuComponent const* cp_m_comp = cp_menu->get_menu_component(i);
14    cp_m_comp->render();
16    if (cp_menu_sel == cp_m_comp)
17      Serial.print("<<< ");
19    Serial.println("");
20  }

It doesn’t take long to see the problems with this approach:

  1. The MenuComponent is tightly coupled to the display hardware because it can’t render without it (Serial in the case above);
  2. The MenuComponent violates the single responsibility principle: it’s responsible for describing the menu system structure and for rendering that structure.

A better approach is to factor out the responsibility of rendering into its own class. In version 3.0.0 this is achieved using the visitor design pattern.

The visitor design pattern is a way of separating an algorithm from an object structure on which it operates.

The MenuComponentRenderer abstract base class decouples the rendering from the menu structure. The interface is composed of render methods for each concrete MenuComponent type.

The client provides a custom renderer which is passed as the only argument to the MenuSystem constructor. When MenuSystem::display() is called, the render method for the current Menu is called.

Depending on what the client wants to display, the render method for the Menu component should call the render method on one or more concrete MenuComponent instances in the menu, passing it a reference to the renderer. The render method on each concrete MenuComponent calls the appropriate render method in the renderer, passing a reference to itself.

This is a form of double dispatch.

The current_item.ino is a succinct example of a renderer:

 1#include <MenuSystem.h>
 3// Renderer
 5class MyRenderer : public MenuComponentRenderer {
 7    void render(Menu const& menu) const {
 8        menu.get_current_component()->render(*this);
 9    }
11    void render_menu_item(MenuItem const& menu_item) const {
12        Serial.println(menu_item.get_name());
13    }
15    void render_back_menu_item(BackMenuItem const& menu_item) const {
16        Serial.println(menu_item.get_name());
17    }
19    void render_numeric_menu_item(NumericMenuItem const& menu_item) const {
20        Serial.println(menu_item.get_name());
21    }
23    void render_menu(Menu const& menu) const {
24        Serial.println(menu.get_name());
25    }

Each concrete MenuItem type is rendered in the same way: by printing its name to the Serial console; a Menu is rendered by rendering its current component.

Using the visitor pattern makes it much easier to reason about how the menu system is displayed and results in code that’s much easier to understand and maintain.