To Do Or Not To Do—That Is the Question

  Peter Long is a consultant within the MCS practice of PricewaterhouseCoopers. He can be contacted at peter.long@uk.pwcglobal.com.

HOW MANY TIMES have you accidentally dropped a manual on your keyboard or clicked your mouse button once too often and been grateful that your favorite word processor or development tool supports an undo option? This article describes how to add undo and redo support to your own Java applications.

Specifically, I review how the Command pattern can be exploited to provide such features. We will enhance a rudimentary drawing application, SimpleDraw, to incorporate undo and redo support.

Command and CommandList
Command

Essentially, the Command pattern encapsulates the invocation of a method and its associated arguments against a target object within a class. These special classes are called Commands. Each Command class supports the method execute(). When execute() is invoked, Command calls the encapsulated method on the target object passing the pre-specified arguments.

Commands are best illustrated by an example. The AWT provides a class to support text editing called TextArea (see java.awt.TextArea). Strings may be inserted into a TextArea by using the insert() method. This method takes a String and an int offset value. When called, the String is inserted into the TextArea at the specified offset.

TextArea textArea = new TextArea( "Hello World." );
textArea.insert( "Wonderful ", 6 );
System.out.println( textArea.getText() );
The above will result in the String "Hello Wonderful World." being output to the Java console.

This code can be used to create a class based on the Command pattern, InsertText, for example. The class would look something like the following:

public class InsertText {
    private TextArea    textArea;
    private String  text;
    private int     offset;

    public InsertText( TextArea target, String str, 
                           int pos ) {
        textArea = target;
        text = str;
        offset = pos;
    }

    public void execute() {
        textArea.insert( text, offset );
    }
}
Now TextArea's insert() operation is encapsulated in a class. Assuming textArea references a TextArea as before, it would be used thus:
  InsertText insertCommand = new InsertText( textArea,
"Wonderful ", 6 );
  insertCommand.execute();
  System.out.println( textArea.getText() );
Again resulting in the same "Hello Wonderful World." message being displayed on the Java console.

Introducing Undo
By itself, this is an expensive way of invoking a single method. The power of using the Command pattern is revealed when we introduce the unexecute() method. This method can be used to support undo.

If we add the following method to the previously defined InsertText class:

  public void unexecute() {
      textArea.replaceRange( "", offset, offset + 
text.length() );
  }
We are able to reverse the effect of executing this command by invoking unexecute(). Now,
  InsertText insertCommand = new InsertText( textArea,
"Wonderful ", 6 );
  insertCommand.execute();
  insertCommand.unexecute();
  System.out.println( textArea.getText() );
causes, "Hello World." to be displayed. This is as if we had never performed the command. We have successfully undone the effect of the command.

The Command Interface
For consistency we standardize commands by defining a Command interface (see Listing 1). This interface defines the two previously introduced methods, execute() and unexecute(). You will also see we have added the pair of test methods canExecute() and canUnexecute()-more about these methods later.

When we create new command classes, we declare that they implement the Command interface. By convention, the constructor for each new class is provided with the necessary arguments so that the command can be executed against a target object successfully.

CommandList
So far, using classes that implement the Command interface only allows us to support a single level of undo. This is fine, however it isn't really very thrilling. How useful would you find an application that only supported a single level of undo?

Commands become useful when we are able to string them together in a sequence. To do this, we develop another class called CommandList (see Listing 2). The CommandList is responsible for executing commands and for remembering the sequence in which they were executed. We use a Stack object (see java.util.Stack) to remember this sequence.

public void execute( Command command ) {
    command.execute();
    executedCommands.push( command );
}
public void unexecute() {
    Command command = (
      Command)executedCommands.pop();
    command.unexecute();
}
As can be seen, execute() takes a command and executes it. The command is then added to the top of the stack called executedCommands. Conversely, unexecute() takes the command from the top of the executeCommands stack and calls its unexecute() method. We can support unlimited levels of undo by creating an instance of CommandList. For each command we wish to be able to undo, we pass it as an argument to the execute() method of the CommandList for invocation.

Redoing An Undone Command
Instead of throwing undone commands away once we have finished with them, to be scavenged by the garbage collector, we can place them on another stack. We call this unexecutedCommands, which will allow us to redo undone commands at a future time. We modify unexecute() to push the undone command on top of this stack.

public void unexecute() {
    Command command = (
    Command)executedCommands.pop();
    command.unexecute();
    unexecutedCommands.push( command );
}
We can now implement a reexecute() method to support the redo of previously undone commands. The command on top of the unexecutedCommands stack is popped and reexecuted by calling the execute() method of the CommandList again.
public void reexecute() {
    Command command = (
    Command)unexecutedCommands.pop();
    execute( command );
}
Some Further Complexity
The unexecutedCommands stack provides an historical record of commands that may be redone. When we execute a new command we must ensure that this historical record is cleared. It doesn't make sense to be able to redo these commands, as the context in which they may be executed has changed. Therefore, execute() has to be further modified so that it clears the unexecutedCommands stack when a new command is executed.

However, when we reexecute commands we do not wish to have the unexecutedCommands stack cleared. We solve this problem by introducing a helper method to be used by both execute() and reexecute(). The helper method, _execute(), executes and remembers the command without clearing the unexecutedCommands stack. execute() clears the unexecutedCommands stack and calls _execute(), while reexecute() calls _execute() after popping a command from the unexecutedCommands stack. The final implementation for these methods is:

private void _execute( Command command ) {
    command.execute();
    executedCommands.push( command );
}
public void execute( Command command ) {
    unexecutedCommands.removeAllElements();
    _execute( command );
}
public void reexecute() {
    Command command = (
      Command)unexecutedCommands.pop();
    _execute( command );
}
Some Convenience Functions
To be useful, the CommandList class also incorporates a number of convenience functions: reset(), which removes all commands from the executedCommands and unexecutedCommands stacks, plus the pair of functions canUnexecute() and canReexecute(). These each return a boolean value, indicating whether the CommandList contains commands that can be undone or redone, respectively. The latter two methods can be used to enforce the preconditions for unexecute() and reexecute() method. See section on Adding Assertions for a further discussion.
public void reset() {
    executedCommands.removeAllElements();
    unexecutedCommands.removeAllElements();
}
public boolean canUnexecute() {
    return !executedCommands.empty();
}
public boolean canReexecute() {
    return !unexecutedCommands.empty();
}
SimpleDraw
To illustrate the real power of the Command interface and CommandList class, we will add undo and redo support to SimpleDraw. SimpleDraw is an Applet that allows a user to create line drawings on a Canvas. The original structure of the Applet is summarized in the UML diagram shown in Figure 1.


Figure 1. UML diagram of the original structure of the Applet.

When the mouse button is pressed within the Drawing area, the motion of the mouse is tracked until it is released. A line is drawn on the Drawing between the start and end positions. The class Drawing (see Listing 4)) extends Canvas and is responsible for listening to mouse events. When the mouse is released, an instance of the class Line (see Listing 5) is created and added to the Drawing via the add() method.

Line is a helper class that records the start and end location of line segments. It also knows how to draw itself on a Graphics context by providing a paint() method.

A new Line is added to a Drawing when the MouseListener interface method mouseReleased() is called as a consequence of the user releasing the mouse button.

public void mouseReleased( MouseEvent event ) {
    Line line = new Line( 
      startPosition, event.getPoint() );
    add( line );
}
startPosition, is a member variable of the Drawing class. It contains the initial position of mouse when it is first pressed.

Adding Undo and Redo Support
Figure 2 illustrates how undo and redo support was added to SimpleDraw (see Listing 3) by adding a CommandList to the Applet. This figure represents the code shown in Listing 3-6. (We added the CommandList to SimpleDraw and not to the Drawing because in a more complex version of this application we may wish to support commands that have nothing to do with Drawings.)


Figure 2. How undo and redo support was added to SimpleDraw.

SimpleDraw exposes the public method execute() so that other objects can create commands and execute them. execute() delegates responsibility to the CommandList that it contains.

Undo and Redo Buttons
For demonstration purposes, SimpleDraw provides a pair of buttons labeled Undo and Redo to allow users to undo or redo previously executed commands. In a real application these would probably be implemented as menu items.

The following helper method is used within SimpleDraw:

private void updateButtons() {
  undoButton.setEnabled( commands.canUnexecute() );
  redoButton.setEnabled( commands.canReexecute() );
}
This sets the enabled states of the undoButton and redoButton buttons, depending on the state of the CommandList contained in the instance variable commands.

SimpleDraw implements the ActionListener interface. The actionPerformed() method traps button-clicked events when either the undo or redo button is pressed. actionPerformed() determines which button was pressed and invokes the appropriate CommandList unexecute() or reexecute() method accordingly.

To ensure that the state of undo and redo buttons reflects that of the CommandList, updateButtons() is called whenever a command is executed or a button pressed.

AddLineCommand
The action of adding a Line to a Drawing is encapsulated in the class AddLineCommand (see Listing 6). This implements the Command interface. The implementations for the execute() and unexecute() methods are shown below:

public void execute() {
       drawing.add( line );
}
public void unexecute() {
       drawing.remove( line );
}
The instance variables, drawing and line, contain the target Drawing instance and Line instance, respectively. These variables are set by the constructor for AddLineCommand.

Drawing mouseReleased() revisited
The implementation of mouseReleased() is changed. Drawing adds a line to itself by creating an instance of AddLineCommand and requesting that SimpleDraw, contained in the variable Applet, executes it. This is similar to the approach adopted with InsertText earlier.

public void mouseReleased( MouseEvent event ) {
  Line line = new Line( startPosition, event.getPoint() );
  AddLineCommand command = new AddLineCommand(
  this, line );
  applet.execute( command );
}
Using AddLineCommands
So where does this place us? As before, the user is able to draw lines within the Drawing area. When SimpleDraw is started, the user is presented with a blank Drawing and a pair of disabled buttons labeled Undo and Redo.

Pressing the mouse while in the Drawing area, dragging it, and then releasing it results in a line being drawn. The Undo button becomes enabled.

If the user clicks on the Undo button, the line is removed from the Drawing. The Undo button becomes disabled, while simultaneously the Redo button is enabled. The user can now quite happily click on the Undo and Redo buttons, adding and removing the previous line to their heart's content.

What happens under the covers?
As a user completes a line segment within the Drawing area, an instance of a MouseEvent is sent to the Drawing. This is received by the mouseReleased() listener method. A Line instance is created, representing the line segment, and inserted together with a reference to the Drawing within an instance of an AddLineCommand object. This command is passed to the SimpleDraw Applet via the execute() method for execution.

The execute() delegates responsibility to CommandList, which executes and remembers the command. Finally, execute() calls updateButtons() to ensure that the Undo and Redo buttons are enabled appropriately.

Later, when the user clicks the Undo button, an ActionEvent is created and processed by the performAction() method within the SimpleDraw Applet. This calls the unexecute() method of the CommandList, resulting in the previously executed command being undone. The enabled state of the Undo and Redo buttons are changed by another call to updateButtons().

FURTHER ENHANCEMENTS
Implementing Undo Using Context

In the examples presented here, we have implemented unexecute() as a method that performs the inverse operation to execute(). However, it may not always be possible to implement an appropriate inverse operation.

For instance, consider the implications of adding an Align Left command to SimpleDraw. Let's assume that our Align Left command would reposition all the lines in a diagram. execute() would be implemented so that the left-most point of each line is vertically aligned with the edge of the line containing the left-most point.

Now, how would we implement unexecute()? We would like unexecute() to move all the lines back to their original positions. In this case, we can't write unexecute() as the inverse operation Align Right.

To solve this problem, we need to write the command so that a context is used. We write the execute() method of Align Left so that it stores the original positions of the lines before they are moved. unexecute() would be written so that it uses this previously stored context to restore all the lines back to their original positions. The use of a context for storing and restoring an original state is a common technique for implementing commands.

Finite Undo and Redo Levels
As written, CommandList supports infinite levels of undo and redo. For real applications it is impractical to allow infinite amounts of resources to be used to support them. We can enhance CommandList, by modifying execute() and unexecute(), so that the stacks they use are not allowed to expand to infinite size. If their respective stacks grow beyond a pre-specified size, we drop the command at the bottom of the stack.

Adding Assertions
You probably noticed that the methods unexecute() and reexecute() in CommandList do not test their respective Stack objects to see if they contain items before they perform a pop() operation. The design for these methods assumes that the respective preconditions canUnexecute() and canReexecute() are true. These preconditions should be enforced by a design by contract mechanism.

Execute Only Commands
It may not always be possible to undo a command once it has been executed because of the state of the application. For example, a command that causes a transaction to be committed to a database may not be able to be reversed. Under these circumstances the canUnexecute() method for the command would return false. The design for CommandList would need to be enhanced so that it can properly handle execute-only commands. CommandList would call reset() once it has executed an execute-only command.

CONCLUSION
I have created the Java interface Command and the class CommandList. These exploit the design pattern Command. Further, I have shown how classes that implement the Command interface and use of the CommandList can be used to add undo and redo support to Java applications.

REFERENCES

  1. Gamma, E., R. Helm, R. Johnson, and J. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995.
  2. Mannion, M. and R. Phillips, "Prevention Is Better Than A Cure," Java Report, Vol. 3, No. 9, 1998, pp. 23-36.

Quantity reprints of this article can be purchased by phone: 717.560.2001, ext.39 or by email: sales@rmsreprints.com.

Featured

Most   Popular
Upcoming Events

AppTrends

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.