Thursday, December 10, 2015

Argument Passing in the Command Pattern


One of the things I appreciate in the command pattern example from the Gang of Four book is that it uses different kinds of receivers. One of the things that bugs me is how the example passes arguments from the client to the command.

The GoF example is a document editor menu. The create-new-document menu item points to a create-document command which is executed when the menu item is selected in the UI. To obtain the filename of the new document, the GoF example invokes an askUser() function which presumably pops up a dialog to await user input. I suppose that is not too horrible, but what if the command argument is already known?

To explore that, I switch from my previous light switch command example to a new take on the VelvetFogMachine—a jukebox-like device intended for playing and organizing songs of the late, great Mel Tormé. This Dart example uses a menu invoker, much like the invoker from the GoF example:
  // Invoker
  var menu = new Menu();
It defines two receivers, the VelvetFogMachine and Playlist instances used to find the ultimate Mel mix:
  // Receivers
  var machine = new VelvetFogMachine();
  var playlist = new Playlist([
    '\'Round Midnight',
    'It Don\'t Mean A Thing (If It Ain\'t Got That Swing)',
    'New York, New York',
    'The Lady is a Tramp'
  ]);
Some of the menu commands available on the user interface might be play-song, add-to-playlist, remove-from-playlist, which would look like:
  // Concrete command instances
  var menuPlay = new PlayCommand(machine),
    menuAddToPlaylist = new PlaylistAddCommand(playlist),
    menuRemoveFromPlaylist = new PlaylistRemoveCommand(playlist);
Like most jukeboxes, the velvet fog machine can play a song by selecting it from a list, then pressing the menu item associated with menuPlay. But how to get that song to the command?

My first instinct is to muck with noSuchMethod to obtain optional execution arguments via reflection. That seems likely to cause trouble. Instead, I start by defining am args property on the Command interface:
abstract class Command {
  List args;
  void call();
}
Then, in the invoker, I assign this property when invoking with arguments:
class Menu {
  void call(Command c, [List args]) {
    if (args != null) c.args = args;
    c.call();
  }
}
The concrete class can use this arguments-property to execute its command:
class PlayCommand implements Command {
  List args;
  VelvetFogMachine machine;
  PlayCommand(this.machine);
  void call() {
    machine.play(args.first);
  }
}
That works, but feels less than ideal. The client code is simple enough:
  menu.call(menuPlay, ['It Had to Be You']);
But the args property just feels awkward.

Hunh. I don't know why I didn't try this first, but the Command interface can be defined to accept optional arguments when invoked:
abstract class Command {
  void call([List args]);
}
I had worried that static typing would bite me when trying to define call() methods when some subclasses required parameters while others did not need them. Dart's optional arguments resolves the problem neatly. Command like "play" that require an argument can support them:
class PlaylistAddCommand implements Command {
  Playlist playlist;
  PlaylistAddCommand(this.playlist);
  void call([List args]) {
    print('==> [add] ${args.first}');
    playlist.add(args);
  }
}
Commands that do not need an argument, like clearing a playlist, can support call() with no arguments:
class PlaylistClearCommand implements Command {
  Playlist playlist;
  PlaylistClearCommand(this.playlist);
  void call([_]) {
    print('==> [clear]');
    playlist.clear();
  }
}
The Dart type analyzer balks if I specify a completely empty call() definition since it does not quite match the interface. But if I specify the argument with the underscore convention for ignoring arguments, I essentially declare a call() with no arguments. That's slightly hacky, but not so much that I am going to lose sleep over it.

I am fairly happy with this argument passing approach. Calling these commands:
  menu.call(menuPlay, ['It Had to Be You']);

  print('--');
  menu.call(menuPlay, [playlist]);

  print('--');
  menu.call(menuAddToPlaylist, ['Blue Moon']);
  menu.call(menuPlay, [playlist]);

  print('--');
  menu.call(menuRemoveFromPlaylist, ['The Lady is a Tramp']);
  menu.call(menuPlay, [playlist]);

  print('--');
  menu.call(menuClearPlaylist);
  menu.call(menuPlay, [playlist]);
Produces the expected results:
./bin/jukebox.dart                                       
Play It Had to Be You
--
Play 'Round Midnight
     It Don't Mean A Thing (If It Ain't Got That Swing)
     New York, New York
     The Lady is a Tramp
--
==> [add] Blue Moon
Play 'Round Midnight
     It Don't Mean A Thing (If It Ain't Got That Swing)
     New York, New York
     The Lady is a Tramp
     Blue Moon
--
==> [remove] The Lady is a Tramp
Play 'Round Midnight
     It Don't Mean A Thing (If It Ain't Got That Swing)
     New York, New York
     Blue Moon
--
==> [clear]
Play
This seems a good stopping point for tonight. Up tomorrow, I will explore the implications of undo in the command pattern when working with multiple kinds of receivers.

Play with tonight's code on DartPad: https://dartpad.dartlang.org/76a20d32a064697bbc8c.



Day #29

No comments:

Post a Comment