Peter Long is a consultant within the MCS practice of PricewaterhouseCoopers. He can be contacted at [email protected].
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
- Gamma, E., R. Helm, R. Johnson, and J. Vlissides, Design Patterns: Elements of Reusable
Object-Oriented Software, Addison-Wesley, 1995.
- 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:
[email protected].