In Progress
Unit 1, Lesson 1
In Progress

Command Pattern Undo

One of the hallmarks of a good user experience is the ability to try things, and then undo them if you don’t like the result. But implementing undo capabilities in a piecemeal, case-by-case basis can get messy fast. In this episode, guest chef Chris Strom demonstrates how the conventions of the Command Pattern enable us to easily add undo-ability in a clean and consistent way.

Video transcript & code

Welcome back to our special miniseries on design patterns with guest chef Chris Strom. As I introduced in episode #461, I’m showing a small selection of episodes from Chris’s Compendious Thunks series.

In the last episode, we met the command pattern. We saw how the command pattern gives us a set of conventions that, when we follow them, neatly decouple program actions from the events that should trigger them.

In today’s episode, Chris dives a little deeper into the command pattern. He’s going to demonstrate one of the great benefits of using this pattern: the fact that, when we encapsulate actions inside of command objects, it becomes very easy to add an “undo” feature to our programs.

Just a reminder: the compendious thunks examples are in the Dart language. But everything you’re about to see is totally applicable to Ruby code as well. Or to any other object oriented language, for that matter. However, if you’re just joining in now and the examples feel a little foreign to you, just jump back to episode 461. There’s a 5 minute introduction to the Dart language in that episode which will give you all the context you need to follow along.

And now, here’s Chris.


The fun stuff with the command pattern starts with undoing commands. After pressing the "Up" button, we ought to be able to hit the "Undo" button to return things to their previous state.

Undo-able commands don't come free. The biggest change will be the need to store commands on a stack as they get issued. That way, a press of the "Undo" button can pop the last command off the stack to be undone.

Since the command knows how to apply the action in the first place, it also get the job of undoing the action. And, since there is no way to "un-call" a function, our simple function commands ain't gonna cut it no more.

Start Coding

[embed_dartpad id="a50790ae0dfbc48498ccb1f445b78eb6" title="The code starts here..."][/embed_dartpad]

First up, let's replace those function commands with objects. Once we've got that, we can add undo support.

As with most patterns, we start with an interface—the Command interface in this case. An interface describes the methods supported by the classes that will implement it. Interfaces are principle cast members in big budget design pattern movies. The concrete classes that implement these interfaces are secondary characters.

All Dart classes have interfaces, but we represent our Command interface as a nice, clean abstract class. To serve as a drop-in replacement for functions, Command implements the Function interface. That, in turn, means that Command classes will need to define a call() method.

Our first concrete command is MoveRight. It will store a single value: the robot / receiver of the action. The constructor requires and assigns this value. And our call() method is going to tell the robot receiver to move right.

With that, we can copy & paste to define the MoveLeft command, the MoveUp command and the MoveDown command.

Now we replace the tear-offs with instances of the command classes. Because these objects define a call() method, they can be called or invoked like functions. That's pretty darn thunky!

So, with MoveRightMoveLeftMoveUp, and MoveDown command replacements, we run our little script and it should just work. Which it does!

As before, we Right, Right, Left, Up, and down -- winding up at (1,0) -- x of 1 and y of zero. If we add one more button press right, we should go to (2,0) and... we did! So that seems to be working.

Again, this works in Dart because Objects can implement the Function interface. You can kinda-sorta do this in less thunky languages like JavaScript using constructors that return functions, but I'll leave that as an exercise to the viewer -- mostly because I wouldn't want to do it!

We still haven't seen the real power of the command pattern, so let's add some undo() methods. First to the command interface. The undo for moving right is simply moving left. The undo for moving Left is moving Right. Up's undo is... down. And Down's is up.

Now that we have our undos, we are ready to take a look at a history stack. Every command needs to go on the same stack, so we'll work with class methods and properties. A singleton would also work here.

History needs a list of all commands that have been issued so far. A static add() method will add commands to that list. Then we declare an undo(), which will remove the last command added, log it, and then tell the command to undo itself. That's it for a basic history class.

Where the add-to-history actually occurs depends on the application. It could be the application itself, the receiver, or the invoker. We'll opt for the latter here, making our Button class add to history whenever a command is invoked.

Back in the application code, we tell History to undo() after moving right three times. And there we go. We undid a move right (which moved the robot left one). The result is that we only move to (2,0). The "undoing instance of 'MoveRight'" message is logged as well. If I comment that out, then I am back to winding up at (3,0). So it really is doing what we want it to. Yay!

[embed_dartpad id="6d85e16809c74d6fd4dad2f67fef4a5c" title="Undoable commands"][/embed_dartpad]

Conclusion

That's the compendious introduction to undoing with the command pattern. Since Dart is so darn thunky, our commands could be both drop-in replacements for the simple function commands and could support undo() actions. The big change here is the history stack. Combined with undoable commands, history allows us to build robust applications that better support how people want and need to interact with modern applications.

There is still more to explore. These were very simple commands and we have yet to touch on macro commands. So stay tuned for more compendious thunky fun!

Responses