Thursday, December 17, 2015

A Happy Dance for Macro Commands


I continue my exploration of the command pattern in Dart by examining macro commands. Macro commands are composites of other commands, which is one of the nice things about commands: that they can be combined.

I have a Robot class that moves in one of four cardinal directions defined in an enum:
enum Direction { NORTH, SOUTH, EAST, WEST }
Commands encapsulate movement in one of those directions along with the receiver of the command, the robot:
var moveNorth = new MoveNorthCommand(robot);
My button invoker class then links buttons with the appropriate command:
var btnUp = new Button("Up", moveNorth);
Which can be pressed to initial movement:
btnUp.press();
This all works thanks to the command pattern.

Today, I want to introduce the happy dance button. Its command will also link the robot to a command:
var danceHappy = new DanceHappyCommand(robot);
And a button will invoke this command when pressed:
var btnHappyDance = new Button("Happy Dance", danceHappy);
To make things interesting, I will make the happy dance a series of random steps. So I import dart:math and seed a random number generator:
import 'dart:math';

enum Direction { NORTH, SOUTH, EAST, WEST }

final rand = new Random(3);
With that, I am ready to dance. Using the raw enum is actually easier to implement than a macro command:
class DanceHappyCommand implements Command {
  Robot robot;
  DanceHappyCommand(this.robot);
  void call() {
    for (var i=0; i<8; i++) {
      robot.move(Direction.values[rand.nextInt(4)]);
    }
  }
}
But, since I am exploring macro commands, I switch to conditionally invoking the appropriate command instead:
class DanceHappyCommand implements Command {
  Robot robot;
  DanceHappyCommand(this.robot);
  void call() {
    for (var i=0; i<8; i++) {
      int r = rand.nextInt(4);
      if (r==0) new MoveNorthCommand(robot).call();
      if (r==1) new MoveSouthCommand(robot).call();
      if (r==2) new MoveEastCommand(robot).call();
      if (r==3) new MoveWestCommand(robot).call();
    }
  }
}
With that, I can move back to my client code to instruct the robot to happy dance:
btnHappyDance.press();
print("\nRobot is now at: ${robot.location}");
Which results in:
$ ./bin/play_robot.dart
[pressed] Happy Dance
  I am moving Direction.WEST
  I am moving Direction.WEST
  I am moving Direction.EAST
  I am moving Direction.WEST
  I am moving Direction.WEST
  I am moving Direction.NORTH
  I am moving Direction.SOUTH
  I am moving Direction.NORTH

Robot is now at: -3, 1
Yay! I am doing a happy dance of my own—that was fairly easy to do. But...

What about undoing that command? This is the command pattern, so I could rewind each command in the macro by invoking their respective undo() method. Instead, I think I will use this as an opportunity to record receiver state in an effort to return to that state as quickly as possible.

So, when the happy dance command is invoked, I make a note of the current x and y positions:
class DanceHappyCommand implements Command {
  Robot robot;
  int _prevX, _prevY;
  DanceHappyCommand(this.robot);
  void call() {
    _prevX = robot.x;
    _prevY = robot.y;
    for (var i=0; i<8; i++) {
      // 8 random moves here...
    }
  }
}
Using that information in an undo() method means that I can get back to the original starting point as quickly as possible:
class DanceHappyCommand implements Command {
  Robot robot;
  int _prevX, _prevY;
  DanceHappyCommand(this.robot);
  void call() {
     // ...
  }
  void undo() {
    var dir;

    dir = robot.x > _prevX ? Direction.WEST : Direction.EAST;
    while (robot.x != _prevX) {
      robot.move(dir);
    }

    dir = robot.y > _prevY ? Direction.SOUTH : Direction.NORTH;
    while (robot.y != _prevY) {
      robot.move(dir);
    }
  }
}
With that, I can instruct the client code to undo a happy dance:
  btnHappyDance.press();
  print("\nRobot is now at: ${robot.location}");
  print("--\n");

  btnUndo.press();
  print("\nRobot is now at: ${robot.location}");
Which results in a happy dance and a quick return back to my starting position:
$ ./bin/play_robot.dart
[pressed] Happy Dance
  I am moving Direction.WEST
  I am moving Direction.WEST
  I am moving Direction.EAST
  I am moving Direction.WEST
  I am moving Direction.WEST
  I am moving Direction.NORTH
  I am moving Direction.SOUTH
  I am moving Direction.NORTH

Robot is now at: -3, 1
--

[pressed] Undo
Undoing Instance of 'DanceHappyCommand'
  I am moving Direction.EAST
  I am moving Direction.EAST
  I am moving Direction.EAST
  I am moving Direction.SOUTH

Robot is now at: 0, 0
Although I might implement both call() and undo() differently in real life, these are still reasonable approaches. They have the added benefit of serving as nice talking points for the command pattern when I discuss it in Design Patterns in Dart, so I will call it a night here.

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

Day #36

No comments:

Post a Comment