Skip to main content

No Regrets with Undo

Posted by tball on December 17, 2004 at 11:47 AM PST

One of the traps to avoid when aging is regret; the more one accomplishes in life, the harder it can be not to regret some of your decisions along the way. Some days it seems life would be perfect if only we had the ability to "hit undo" on those mistakes. Life doesn't have such an undo facility, but happily we can easily code them into our applications. "Easily?", most developers might ask, cringing at the thought of an "undo" product requirement especially if it has to be added to an existing, working project. Traditionally it's been considered very difficult to determine the correct undo points and to test that the facility works correctly. But with a good design, a robust undo facility can indeed be easy to implement.



Lately I've been focusing on isolating model and controller code in my application to simplify its design and make it more testable. One key to successful unit testing is having tests that run quickly, so you aren't inclined to skip running them in a crunch (when you need them the most). Independent data models can be tested very quickly since very few secondary classes need to be loaded for the JVM to execute each test. Now, Swing has a powerful, fully-featured undo facility, but it is difficult to use in a way that doesn't cause lots of other Swing classes to be loaded (here are the Swing classes from the UndoManager's transitive closure, generated with this tool).



While working on Jackpot, we used a much simpler undo facility that James Gosling wrote, which has been improved and incorporated into Huckster's Undo class. This class defines a single Undo class, to which custom Entry instances are added which implement the undo and redo methods. Since an undo command from the user's perspective can consist of several associated state changes to the model, you define the start of each action by calling Undo's newCommand method. The application's undo and redo commands invoke Undo.undo() and redo(), not specific entries. That's all there is to his framework.



To use this facility in your application, find each place where your model is changed. Ideally, you have already refactored your application so that all of these mutations are in small, separate methods (ideally in the model class itself); if not, do this refactoring first as it will help your design regardless of whether it supports undo or not. Here is a simple example:

    public void setLocation(int x, int y) {
        currentX = x;
        currentY = y;
    }

Next, define a singleton Undo instance for your application (we'll call it "undo"). Then for each mutating method, add a new Undo.Entry type to undo, moving the method's current code into the Entry's redo method:
    public void setLocation(final int x, final int y) {
        undo.add(new Undo.Entry() {
            public void undo() {
            }
            public void redo() {
                currentX = x;
                currentY = y;
            }
        });
    }

The undo command still doesn't work, but all existing unit tests should pass, since adding an Undo.Entry to the Undo instance invokes the entry's redo method. Next, figure out the state you are changing and define a n instance variable for each, initializing it to the current value before the change:
    public void setLocation(final int x, final int y) {
        private final int oldX = currentX;
        private final int oldY = currentY;
        undo.add(new Undo.Entry() {
            public void undo() {
            }
            public void redo() {
                currentX = x;
                currentY = y;
            }
        });
    }

Finally, determine how to undo the redo method's code. If you have refactored to a fine granularity (important for maintainability and testability), it should be fairly obvious:
    public void setLocation(final int x, final int y) {
        private final int oldX = currentX;
        private final int oldY = currentY;
        undo.add(new Undo.Entry() {
            public void undo() {
                currentX = oldX;
                currentY = oldY;
            }
            public void redo() {
                currentX = x;
                currentY = y;
            }
        });
    }

Your code now supports fine-grained undo. To define user-level undo command actions, find where the start of each command is and call undo.startCommand() before the first modification. You can move these startCommand calls around, add or delete them depending on the UI spec and/or what "feels right" during user testing. If you want to flush the undo list (say, when saving a document), just replace the application's Undo instance with a new one.



Testing an undoable model is pretty straightforward: you need deep-copy and equality methods. Some classes already have these, such as the collections classes, but for custom data structures you'll probably have to write them yourself. The good news is that these methods increase the value of your model as a separate data type, rather than just being a testing chore. I added duplicate and equals (with hashCode, of course!) to an abstract syntax tree model for test purposes, and later found them useful for productive code enhancements.



Now if anyone has a way I can undo my own past, please let me know. There are several old JDK warts I would love to erase!

Related Topics >>

Comments

Undo has evolved

The Java framework now has a complete and easy to use undo facility. Find a simple tutorial on Java undo/redo in my blog on www.processworks.de.

Swing Undo has been around since it's first release

That's a great tutorial for an old, but underused library. The best line is "Please be aware that UndoManager is in the Swing package, but this does not mean that you must have a Swing GUI to use it!" Often server-side developers ignore all the supposedly client-side stuff out there, when in reality it's just good application-support code.