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, 5While 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