XPlorations
| Command, Interpreter, Visitor |
December, 2000 |
| Design patterns are interesting in themselves, but it's
useful to be able to move between patterns while refactoring a system. We'll
look at how you can move between command, interpreter, and visitor. |
Many interactive programs allow a user to perform one of a number of actions.
The code for this tends to begins its life scattered around the system.
This usually shows up in one of two forms. In the first form, the code is in
a big switch or if statement:
if (control.equals("Save")) {
// save the file
} else if (control.equals("Cut")) {
// cut the selection
} else {
// etc. - on and on
}
This is bad form because the handler routine becomes a bottleneck for
changes. Any new operation means changing code rather than adding
code; thus changes in one feature might break completely unrelated ones.
The second form has the code attached to widgets. For example:
cutMenuItem.addMenuListener (new CutListener());
:
public class CutListener {
public void handle(MenuEvent e) { /* cut code */}
}
This code is better in that it keeps separate functions apart, but it's still
flawed because GUI code tends to be intermingled with Model code.
Command
There are several forces that might make you wish for a more general
solution:
- A need to trigger the same action from multiple places (e.g., from the
menu or from a button). The "CutListener" code above might work, except that
it's customized for menus rather than some other widget type.
- A need to support macros. This is a similar problem, in that there may be
two things trying to perform an action: the original button click or the macro
subsystem. The macro code can be even more complicated if it's dealing with a
sequence of commands.
- A need for "undo" support. Again, there are two places that might want to
perform an operation: the original button or menu item, and the undo/redo
subsystem.
The bits of "action" code tend to represent functions, but we may find it
easier to "objectify" them. The Command pattern exists to do this.
Suppose we have this code:
:
Function A
:
Function B
:
(Make sure to test after each step.)
- Create a parent Command class:
Command {
abstract void execute();
}
- For each bit of code (e.g., Function A), introduce a new class that's a
subclass of Command (e.g., ACommand):
ACommand extends Command
Do this one command at a time.
Compile and test the new class.
- Copy the function code to the subclass' execute() method:
ACommand {
execute() { /* Function A body
*/ }
}
- Replace the original site for function A:
:
new ACommand().execute();
:
Function B
:
Compile and test.
- Repeat for each function.
- Determine whether those call sites each need to create a new instance of
the command (subclass) each time. You may find you can create an instance of
the command and pass it around. (For example, a menu and button might be able
to share the same instance of a command object.)
At this point, you're ready to introduce macros, redo, etc. as in the
standard Command pattern.
Command and Composite
For macros, or just convenience, it can be convenient to build commands out
of other commands. For example, some systems let the user record a sequence of
commands as a saved action.
In this situation, make Command a Composite:
public
class ACommand extends Command {...}
public
class CompositeCommand extends Command {
Vector commands; // etc. ...
public void execute() {
// for each element c of commands, do c.execute()
}
}
The CompositeCommand represents a command that can hold and execute a sequence
of other commands.
Interpreter
At this point, we've almost become an interpreter. There are three key
differences:
- Interpreter generally references an external context
- The composite types are usually broadened to more than simple sequences
(e.g., separate composites for "if" versus "repeat").
- Interpreters typically don't support "undo", as the context becomes so big
it's not feasible to hold all the state in memory.
How do you get a language into the form that Interpreter expects to work with?
There are typically two approaches:
- Programmatic: The caller is responsible for building the object structure
it wants to interpret.
- Parsed: There is a separate routine that takes a string (the code to be
interpreted), and it is responsible for understanding the string so it can
create the objects.
If you're setting out on a plan to invent a new language, you should first
consider whether an existing interpreter could handle what you need (perhaps
with a new library or some minor tweaking).
To Visitor
You have this:
public class Command {
abstract
void execute();
}
public class ACommand extends
Command ...
public class BCommand extends
Command ...
But you find there's another process that wants to look at the same nodes:
public class Command {
abstract
void execute();
abstract
void display();
}
Then another function comes along: it doesn't need to change the command
structure, but needs to perform a new function. You get tired of updating all
the command nodes again. You may decide to move to the Visitor pattern. (See
Design Patterns by Gamma et al. for the gory details.)
- Create a Visitor class. For each command class that exists, give it an
accept method:
public void visit(ACommand) {}
public void visit(BCommand) {}
Compile and test.
- Add an accept() method to each Command node. Each one should have
an implementation like this:
public void accept (Visitor v)
{
v.visit(this);
}
Compile and test.
- For each function F on the Command nodes, create a subclass of Visitor,
FVisitor. Copy its body from the corresponding command to the corresponding
method. Compile and test.
- Create a new test of the new Visitor. It should create an instance of the
visitor, and call the command's accept() method. Compile and test.
Create more tests as needed. This should function exactly as the original.
Compile and test.
- Working one at a time, turn all calls to the function into calls on the
visitor. Compile and test.
- There should be nothing left calling the function. Delete it from each
command class. Compile and test.
- Repeat for the other functions.
What you're left with is the new Visitor form.
Warning
Here's my warning: Use Visitor with care.
I've migrated to Visitor twice in my life. I've ended up ripping it back out
both times.
Why? Because Visitor is not happy when the command structure changes. Even
when I think I've got that nailed down, it turns out that the command structure
is what changed, but I wasn't adding more functions.
Is Visitor a bad or useless pattern? No - it's chasing something important. I
have it clustered with the idea of "double dispatch" (depending on two types to
know what to do) and mixins (assembling code from small bits of functionality).
I think of "futuristic" approaches like aspect-oriented programming [http://www.parc.xerox.com/csl/projects/aop/]
or subject-oriented programming [http://www.research.ibm.com/sop/]
as embedding visitor in a way that the system can automatically take care of
changing the commands or the visitors. Visitor in Java feels about as clumsy as
a simulated object in C: it can be done manually, but only in a disciplined way.
Returning
It's not hard to go back from Visitor to Interpreter:
- Work one Visitor at a time. Add a function to the command class
corresponding to the Visitor. Compile and test.
- Copy the corresponding visitor method code to the method on the various
Command subclass objects. Compile and test.
- Add new tests for the new command objects. Compile and test.
- Working one at a time, replace each Visitor call site with a call to the
corresponding function. Compile and test.
- Once all uses of a Visitor are gone, delete it. Compile and test.
- Continue until all Visitors have disappeared.
Conclusion
We've demonstrated how code can be changed to use a Command pattern, then an
Interpreter, and a Visitor, and back.
Resources
[Written 9-6-2000; minor edits 12-7-2000.]
|