Tuesday, December 15, 2015

Redo Command


I have undo. So what about redo?

Over the past several days, I have explored undo in the command pattern in Dart, which leaves me a little curious about redoing / re-executing commands. I expect that they are fairly simple, but my expectations are rarely met in these matters. While I am at, I am still evaluating last night's robot command example as being worthy of using in Design Patterns in Dart—I worry that the robot example uses commands that are a bit too literal and is too simplistic.

Editable DartPad of last night's robot command example: https://dartpad.dartlang.org/3b2ac3f421db89c2b56a.

My first instinct with both undo and redo in this new example is to make them command buttons just like the other buttons on the "UI." This would allow me to press() these buttons just like the other buttons on the UI:
  btnUp.press();
  btnUp.press();

  btnUndo.press();
  btnRedo.press();
After considering this for a while, I think better of it. The undo and redo buttons look like the regular command buttons, but they do not behave that way. Specifically, regular command buttons store commands in history for… undoing:
class Button {
  static List _history = [];

  String name;
  Command command;
  Button(this.name, this.command);

  void press() {
    print("[pressed] $name");
    command.call();
    _history.add(command);
  }

  static void undo() {
    var h = _history.removeLast();
    print("Undoing $h");
    h.undo();
  }
}
But I cannot invoke that same press() button lest the undo command get added to the undo history (the second of two undos would then attempt to undo the first, which is not right). I could create a fake button class specifically for these kinds of actions, except these fake buttons would wind up calling fake commmands:
    var undo = new UndoCommand(),
      redo = new RedoCommand();
These are not real commands in the command-pattern-sense because they do not associate a receiver with an action. The receiver of these action is the Button class. An undo command would just know this without having to be told:
class UndoCommand implements Command {
  void call() { Button.undo(); }
}
Admittedly, this is likely my code telling me that I should move history out of the Button class (what if a slider is added to the UI?). I will look into that another day. For now, I bypass the undo and redo buttons and just call those actions directly:
  btnUp.press();
  btnUp.press();
  btnUp.press();

  Button.undo();
  Button.undo();
  Button.redo();
With that out of the way, what of redoing?

The simplicity of commands for my robot makes undoing and redoing simple. The move-north command, for instance moves north when doing (or re-doing) and moves south when undoing:
class MoveNorthCommand implements Command {
  Robot robot;
  MoveNorthCommand(this.robot);
  void call() { robot.move(Direction.NORTH); }
  void undo() { robot.move(Direction.SOUTH); }
}
That is a fine implementation. I only worry about this in book format because it is so simple that it lacks the ability to illustrate the power of the pattern. Again, something to worry about another day.

After implementing similar undo() and redo() methods in the remaining command classes, I can teach the Button class how to use them. Undo is easy enough, but if I am to retain the option of re-doing and undone command, I need a place to store undone commands. Which means that I need a list of done commands and undone commands:
class Button {
  static List _history = [];
  static List _undoHistory = [];
  // ...
}
That aside, the first-pass implementation of undo() and redo() are simple enough:
class Button {
  static List _history = [];
  static List _undoHistory = [];
  // ...

  static void undo() {
    var h = _history.removeLast();
    print("Undoing $h");
    h.undo();
    _undoHistory.add(h);
  }

  static void redo() {
    var h = _undoHistory.removeLast();
    print("Re-doing $h");
    h.call();
    _history.add(h);
  }
}
That gets me what I want. If I move the robot 6 paces up/north, then undo two of those steps, then redo one of the undos:
  btnUp.press();
  btnUp.press();
  btnUp.press();
  btnUp.press();
  btnUp.press();
  btnUp.press();

  Button.undo();
  Button.undo();
  Button.redo();

  print("\nRobot is now at: ${robot.location}");
Then I wind up 6 - 2 + 1 = 5 paces to the north:
$ ./bin/play_robot.dart
[pressed] Up
  I am moving Direction.NORTH
[pressed] Up
  I am moving Direction.NORTH
[pressed] Up
  I am moving Direction.NORTH
[pressed] Up
  I am moving Direction.NORTH
[pressed] Up
  I am moving Direction.NORTH
[pressed] Up
  I am moving Direction.NORTH
Undoing Instance of 'MoveNorthCommand'
  I am moving Direction.SOUTH
Undoing Instance of 'MoveNorthCommand'
  I am moving Direction.SOUTH
Re-doing Instance of 'MoveNorthCommand'
  I am moving Direction.NORTH

Robot is now at: 0, 5
While that works, I definitely need to rethink where the history is stored. The Button class now has more history-related code than it does button-related code—a definite no-no. That coupled with my inability to support meta-buttons like undo/redo mean that tomorrow I explore retaining history in the application instead of the Button class. And maybe a happy random dance.

Play with the DartPad of the robot code so far: https://dartpad.dartlang.org/8fa8a3b4e0765e657e8a.


Day #34

No comments:

Post a Comment