Saturday, December 12, 2015

What's in an Invoker Name?


I may be conflating concepts as I explore the command pattern for Design Patterns in Dart. Of course, I may perfectly OK in my current thinking, but tripping over terms in my mind lends doubt. So tonight I take a closer look at the invoker in the command pattern.

The invoker is the primary motivator in the pattern, so it behooves me to get it right. I like the menu item example in the Gang of Four book. It makes sense that actions need to be associated with menu items—that commands need to be associated with invokers. It then follows that the application (client) needs to instantiate these commands and associate them with menu items. The remainder of the pattern is the receiver and action of the action (e.g. the application receiver and the open action).

But I think the menu in my Velvet Fog Machine (it plays Mel Tormé, of course) is not quite right:
class Menu {
  List _history = [];

  void call(Command c, [List args]) {
     c.call();
    _history.add(c);
  }

  void undo() {
    var h = _history.removeLast();
    h.undo();
  }
}
Thanks to last night's efforts, that works just fine, but perhaps I should have stuck with the GoF's menu item instead of calling it a menu. Maybe it doesn't make a difference to the pattern. If nothing else, I have the feeling it would be a stronger example if it stuck to the menu item, so let's see.

If I were first approaching this problem, I would start not with the commands, but with the menu items on the velvet fog machine's user interface. There would be menu items for playing a single song and for manipulating playlists. When the code is initialized, it would have to associate these menu items with commands. Or, in Dart:
  // Invokers
  var menuPlay = 
    new MenuItem("Play", play);

  var menuPlaylistAdd = 
    new MenuItem("Add to Playlist", addFromPlaylist);

  var menuPlaylistRemove = 
    new MenuItem("Remove From Playlist", playlistRemove);

  var menuPlaylistClear = 
    new MenuItem("Clear From Playlist", playlistClear);
The commands do not change here, just the change from menu to menu-item. The resulting MenuItem class would then need to look something like:
class MenuItem {
  static List _history = [];

  String name;
  Command command;
  MenuItem(name, command);

  void call([List args]) {
    command.call();
    _history.add(command);
  }

  static void undo() {
    var h = _history.removeLast();
    h.undo();
  }
}
I am unsure about the history storage with this switch—that is likely a topic for another night. What I am sure is that the client code that follows now feels stronger to me.

If I were using the Velvet Fog Machine to play It Had to Be You, I would press the code for the song along with the Play button (note to self, maybe button here instead of menu item). I would expect that to be already associated with a command. With the previous Menu approach, I wound up calling the Menu instance with the command and the song:
  menu.call(play, ['It Had to Be You']);
  menu.call(play, ['Cheek to Cheek']);
Maybe that is still an acceptable implementation of the command pattern, but it seems awkward at the very least.

With the new MenuItem approach, a press of the Play button now results in calls like:
  menuPlay.call(['It Had to Be You']);
  menuPlay.call(['Cheek to Cheek']);
That makes me much happier. The difference between menu.call(play, ...) and menuPlay.call(...) may seem small, but it is much closer to the original description the GoF. Additionally, this new approach associates the command from the outset:
  var menuPlay =
    new MenuItem("Play", play);
Previously the application had to remember this association each time it needed to run the command.

I have a clearer picture now of the pattern. I also see the invoker-as-prime-mover as an important concept in the pattern. As an added bonus, I think I see a bug in undos that I was trying to find last night. So it looks like I will be back in undo land tomorrow.

Play with the code on DartPad: https://dartpad.dartlang.org/efac76d03df17f6e3920.


Day #31

No comments:

Post a Comment