So where does history get stored in the command pattern? The Gang of Four book only mentions in passing that the "application" holds the history. I wound up putting history in the command invoker as I was experimenting. That it is looking like a mistake.
The problem is my Dart invoker does more history management than it does invoking:
// Invoker class Button { static List _history = []; static List _undoHistory = []; 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(); _undoHistory.add(h); } static void redo() { var h = _undoHistory.removeLast(); print("Re-doing $h"); h.call(); _history.add(h); } }Do you see the code that actually invokes commands? Probably not without looking really close because of all those static methods.
It's never a good sign if you have a bunch of static classes and properties like that. My code is begging me to extract it out into a separate class, which I do:
class History { List _undoCommands = []; List _redoCommands = []; void add(Command c) { _undoCommands.add(c); } void undo() { var h = _undoCommands.removeLast(); print("Undoing $h"); h.undo(); _redoCommands.add(h); } void redo() { var h = _redoCommands.removeLast(); print("Re-doing $h"); h.call(); _undoCommands.add(h); } }With that, my button invoker is much, much nicer:
class Button { String name; Command command; Button(this.name, this.command); void press() { print("[pressed] $name"); command.call(); } }Now I need to figure out how to get commands into history. Do I alter the
press()
method of Button
to return the command so that client code can store history or do I make the invoker responsible for storing history. I opt for the latter, mostly because I do not want to clutter up my client code with a call to history.add()
for each button press.I do opt to convert the
History
class into a singleton and its add()
method into a static method (static methods do have their place):class History { // ... static final History _h = new History._internal(); factory History() => _h; History._internal(); static void add(Command c) { _h._undoCommands.add(c); } // ... }There is never a reason for more than one history object, so a singleton makes sense. Converting the
add()
method to a static method is a judgment call, but I wanted to be able to invoked History.add()
when adding to history in the invoker:class Button { // ... void press() { print("[pressed] $name"); command.call(); History.add(command); } }With that, I can get access to the history singleton in my client code and use it to undo and redo commands as much as I like:
var history = new History();
btnUp.press();
btnUp.press();
btnUp.press();
history.undo();
history.undo();
history.redo();
print("\nRobot is now at: ${robot.location}");
That does the trick as the robot code that responds to these commands moves three paces to the north (in response to three button-up presses), undoes two of those steps, then re-does one of the undoes:$ ./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 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, 2For a grand total of 2 steps forward.
I am reasonably happy with the resulting code, but there is another benefit to making this change. I can now treat history as a command receiver in the command pattern:
// Receivers
var robot = new Robot();
var camera = robot.camera;
var history = new History();
// ...
var moveNorth = new MoveNorthCommand(robot),
// ...
undo = new UndoCommand(history),
redo = new RedoCommand(history);
// Invokers
var btnUp = new Button("Up", moveNorth);
// ...
var btnUndo = new Button("Undo", undo);
var btnRedo = new Button("Redo", redo);
btnUp.press();
btnUp.press();
btnUp.press();
btnUndo.press();
btnUndo.press();
btnRedo.press();
print("\nRobot is now at: ${robot.location}");
The undo/redo commands only need to store the history instance so that they can call the appropriate action in response to a button press:class UndoCommand implements Command { History history; UndoCommand(this.history); void call() { history.undo(); } }That history is itself a command now feels like a nice symmetry. In addition to the improved command implementation, I realize that I have an unfortunate habit of using static methods when writing exploratory code. I need to watch that or I am going to get myself intro trouble one of these days. But for today, I am safe.
Play with the code on DartPad: https://dartpad.dartlang.org/87111ba94d21ac059a42.
Day #35
No comments:
Post a Comment