The Source for Java Technology Collaboration
User: Password:



Tom Ball

Tom Ball's Blog

No Regrets with Undo

Posted by tball on December 17, 2004 at 11:47 AM | Comments (8)

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!

Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • Slick-o-matic. I'm going to add that to my application!

    Posted by: jessewilson on December 19, 2004 at 02:40 PM

  • I think where your code says:

    private final int oldX = x;
    private final int oldY = y;
    you meant to have:

    private final int oldX = currentX;
    private final int oldY = currentY;
    Other than that, it's an impressively simple idea.
    Thanks for sharing it.

    Posted by: grlea on December 19, 2004 at 04:04 PM

  • You are right -- thanks for the correction!

    Posted by: tball on December 19, 2004 at 07:33 PM

  • Someone once said, it's the things you don't do in life that you end up regreting, not the things you did.

    Posted by: uhf on December 20, 2004 at 01:15 AM

  • I like it. Very simple. Thanks.

    I wish the syntax were even simpler; it would be nice to get rid of the finals... but I suppose it

    Posted by: jtr on December 21, 2004 at 10:26 AM

  • ... (continued) is a language change to add proper closures to Java. :)

    Posted by: jtr on December 21, 2004 at 10:26 AM

  • I like the implementation. The class itself could do with some serious Javadoc, but that might be true of a few of my own classes too :)

    I do have a question though. Why do the whole startCommand thing at all? Whenever I've implemented the Command pattern in my programs I've always assumed that the smallest atomic unit was a single command. They were not indivisible and therefore there was no reason to mark a start and end point for a transaction.

    To me that's one of the differences between SQL (which implements an extremely simple language and you are forced to tell it transaction boundaries) and say Prevayler (where each Command object that modifies the model is considered to be a complete transaction for the purposes of keeping your persistence ACID). That is, if you need to both debit one account and credit another and neither should ever happen in isolation then you structure your commands so every command only includes both actions as a unit and do not provide one with any other option.

    Posted by: johnmunsch on December 21, 2004 at 01:23 PM

  • The reason for startCommand is to provide very lightweight transaction support (instead of hitting commit to apply changes, just don't undo them before the next transaction starts). For some models, you are right that each change is atomic, but for others, one user action causes several related changes to the model, which all have to be un-done or re-done as a single unit so the model remains consistent.

    My current project (Jackpot) modifies abstract syntax trees; if I rename a method, for example, all of its references need to be updated at the same time or the code breaks. With the Undo class, each change can have a separate undo entry (easier to implement and test), but by specifying startCommand before applying the user's action, all changes are batched into a single transaction for robust undo and redo.

    Posted by: tball on December 21, 2004 at 01:43 PM





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds