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()
2{
3 Serial.println("");
4
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());
12
13 if (cp_menu_sel == cp_m_comp)
14 Serial.print("<<< ");
15
16 Serial.println("");
17 }
18
19 // Code ommitted
20}
There are two problems with this approach:
- Complex decision blocks would be needed to render each
MenuComponent
type differently, resulting in code that’s difficult to read and maintain; - 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 {
2public:
3 void render() const {
4 Serial.println(_name);
5 }
6};
7
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();
15
16 if (cp_menu_sel == cp_m_comp)
17 Serial.print("<<< ");
18
19 Serial.println("");
20 }
21}
It doesn’t take long to see the problems with this approach:
- The
MenuComponent
is tightly coupled to the display hardware because it can’t render without it (Serial
in the case above); - 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>
2
3// Renderer
4
5class MyRenderer : public MenuComponentRenderer {
6public:
7 void render(Menu const& menu) const {
8 menu.get_current_component()->render(*this);
9 }
10
11 void render_menu_item(MenuItem const& menu_item) const {
12 Serial.println(menu_item.get_name());
13 }
14
15 void render_back_menu_item(BackMenuItem const& menu_item) const {
16 Serial.println(menu_item.get_name());
17 }
18
19 void render_numeric_menu_item(NumericMenuItem const& menu_item) const {
20 Serial.println(menu_item.get_name());
21 }
22
23 void render_menu(Menu const& menu) const {
24 Serial.println(menu.get_name());
25 }
26};
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.