As you might have guessed, I pay siginificant attention to UI/UX issues. Since I’ve been implementing undo functionality recently, I thought I’d share a few thoughts about it.
On the surface, undo seems easy: you record all changes you have made and revert the last one on demand. When you look at details, however, things can get messy.
First of all, how do you actually “record all changes”? You might record the state after every change (probably simple in many applications, but potentially a memory bottleneck). You might just record the deltas (better from the memory perspective, but a bit more involved). In the case of Logeox, however, I decided to do something even different. Instead of recording the state after each “change” (i.e., turtle command), I record the commands themselves. Since they are sufficient to completely recreate the state at any point of time, undoing the last command is easy: you just delete the last link in the chain and replay them again. (Theoretically, it means that undoing a change is a lot more expensive timewise than issuing commands, but I do not expect this to be a real problem. And if it ever becomes one, I can easily add some caching, like e.g. recording the full state every 40 commands or something like that.)
Then there comes another question: what exactly a change is? In my case, I have a few commands: go forward, turn left/right, pen up/down. Should all of them be undoable? This would be the simplest thing to do (and the first one I did), but probably not the best. After consulting half of my users (i.e., my dear wife), I decided that only actual turtle movements should be undoable. (By movement, I mean a command changing the turtle’s position, so turns are not included.)
This raises another question. Assume the following sequence of user actions: go forward, turn right, undo. Should the undo restore the direction from before the “go forward” command? I think the answer is positive (and fairly obvious). Consider now this: pen down, go forward, pen up. Should the undo change the state to pen down again? This time the answer seems less obvious, but still affirmative. And so I did it this way.
And how did I actually implement all these? It turned out to be pretty simple. I added another method to the TurtleCommand
object, isUndoable
, which returns false for each command except GoForward
. (I briefly considered making it return false by default and only overriding it in GoForward
. After a while, I decided that I like defining it explicitly in each command a bit better. Yes, it looks like code duplication, but (1) it makes me think before deciding – for each and every command – whether it should be undoable, and (2) it felt kind of wrong to me to hardcode the choice that happens to be more common in the parent class.) And Undo
, instead of just removing the last command from the list, keeps removing commands until the first one with isUndoable
returning true. (And while at that, I fixed a small but irritating bug with the app crashing when trying to undo when no commands were yet recorded.)
By the way, if this were JavaScript, I’d probably use a constant property (i.e., a field) instead of a method. In Java, however, there would (should?) be a getter for it anyway, so – since it’d be constant – I see no point in having the field at all. I’m pretty sure there exist a design pattern about a method whose body consists of return false;
and nothing else.
And now that I googled it a bit and gave it a minute of thinking, I guess there’s even a better way: to have the isUndoable()
method return a field, initialized to the proper value by the constructor. And BTW, googling for that revealed a whole mess of opinions and possibilities of having fields with various qualifiers – static
, final
, private
etc. I’m not sure what that means. Definitely OOP in Java is a complex thing. The question remains whether it is complicated because it has to – IOW, the matter is complicated, and the language design reflects that – or because the design is lousy. That I don’t know; I’d have to study closely other object systems (Javascript? CLOS? Smalltalk?) to be able to compare them and form the opinion. Yet another “when-I-break-my-leg-and-have-a-lot-of-time-in-hospital” project…