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.]
|