When designing applications focused on the creation or modification of data, like text or image editors, for example, a common desire for your end-user is the ability to undo or redo their actions. It’s an important consideration, because the knowledge that steps can be safely and easily undone gives your users confidence in using your app.
So, you’ve decided to try integrating an undo system into your project, but you’ve never written anything like that before. How do they work? Where do you even start? This article aims to give you a little direction by introducing you to how undo systems work and how to write one.
An “Undoable” Counter
Let’s start this off with a simple (and extremely common) example: a counter!
Mind-blowing, right? This code defines a function for generating counters that can be incremented or decremented using the
decrement methods! …yeah, there’s not much to it. And its behaviour is similarly predictable:
While nothing to write home about, this code provides the perfect place to begin exploring undo operations. It’s easy to understand what it’s doing, and it has state data that is subject to change. Let’s see if we can add an undo function to it!
The easiest way we could implement undo operations would be to keep track of the counter’s
value whenever it changes, and add it to a “history” array that we can use to revisit previous states. We’ll also keep a pointer to the specific index of the array that represents the present value of the counter in a variable called “position”. With both a history array and a position pointer, we no longer need to keep a separate
value variable, and will replace it with a function that returns the element of
history at the current
The implementation might look like this:
With this in place, all we need to do to enable undo and redo for our state changes is decrementing and incrementing the history position! Knowing this, we can go ahead and add the undo and redo methods:
Simple, right? All that’s left is to modify the
decrement methods to actually push new values to
history and update the
position accordingly. We’ll add a new method,
setValue, to assist with that:
setValue function accepts a new value, removes any states ahead of the current position in history (making a new change after a series of undos should clear any existing ‘future’ states), pushes the new value to
history, and points
position to the new state. The
decrement methods now use it, as well as the new
value method, to perform the same tasks they did before.
All in all, our new “undoable” counter should look like this:
And we can try it out with some simple driver code:
Success! Our new object is capable of remembering its past states and revisiting them with its
A More Robust Approach
The previous approach is a good introduction to history traversal, and works well enough for tracking a single, simple variable. However, trying to track more complex data exposes some shortcomings. What if you wanted to undo changes made to an object this way? Would you deep copy the entire object into the history at every change? If the object contains a significant amount of data, making many small changes to it could quickly start to eat up all your available memory!
To avoid that, we’ll turn to a common programming design pattern: the Command pattern. Explaining the pattern in-depth is beyond the scope of this article, but for our purposes you can think of it as taking the actions you wish to perform on your data and turning them into objects that can be passed around, stored, and executed at any time by a controller.
Abstracting the actions into command objects allows us to store a history of all the changes we’ve made to an object, rather than a history of that object’s states.
Let’s take this new concept and make it familiar by applying it to a similar situation as our last example.
So in this case we have yet another counter. This time, however, its state is an object with multiple properties: a
name and a
count. It also doesn’t have any built-in increment or decrement methods. We’ll be writing commands to take on that responsibility!
In the case of an undo/redo system, a command should be an object that at the very least contains two methods:
undo. As the names imply,
execute performs an action on the data, and
undo restores it to its previous state.
Let’s see what
increment might look like when implemented as a command:
When a new increment command is created, it takes in the counter object it is modifying as a parameter, and stores the current value of the counter in a variable called
execute method increases the value of the counter’s
count by one, and its
undo method restores its value to the state stored in
Given the simplicity of incrementing a counter, the undo method could also be implemented as simply decrementing it. However, storing the previous state of any changed variables is a more flexible solution that can apply to more complex operations, so I’ve elected to demonstrate that technique here.
Decrementing is essentially the same but with subtraction instead of addition in its
But this is only half of the story. If we’re making commands, we also have to have a way to store them, apply them, and manage a command history to enable a chain of undos and redos! Let’s define a command manager to take care of that for us.
Whoa! That’s a bunch of new stuff! Some of it should look familiar, though. The managing of
position, as well as the history traversal in the
redo methods, are pretty much the same as what we did in the last example. So we’ll focus instead on what’s new here.
commands are all constants used to ease the process of selecting a particular command to generate. The string constants prevent string input errors, and the
commands object exists to take the place of a switch statement, allowing you to access a particular command generator function by its associated string. You can learn more about using objects in place of switches here, if you like.
createCommandManager takes an object as a parameter in its creation to use as a target to apply its commands to (not always necessary for the pattern, but it fits well with the rest of the implementation here). It returns an object with three methods:
doCommand is where the magic happens. First, it clears any future commands left by undos, much like
setValue does in our last example. It then checks to see if the command string it was passed exists in the
commands object, and if so, it creates a new command object from it, targetting
target. It pushes the new command to the history array, then executes it, thereby applying its changes to
As mentioned earlier,
redo are very similar to the previous example, but now that we’re dealing with a history of commands,
undo runs the command’s
execute) to update the object’s state.
Now that we’ve got all the pieces of the puzzle together, let’s try out some driver code and see it all in action!
Yes! It works as expected! We’re able to increment and decrement our counter and undo changes to it via the command manager, all without touching or having to keep track of the name variable!
If you’ve been following along, feel free to pat yourself on the back, because you’ve learned how to time travel! At least in the context of your application state. :)
If you want to learn more about the command pattern and undo systems, feel free to check out these resources I used in writing this article: