In the November 2000 issue of Java Pro,
James Cooper surveyed the well-known Visitor pattern. He explained that
every element to be visited needs to cooperate by providing an accept()
method. This constraint can sometimes be very burdensome. However, a
technique is presented in Java 2 that eliminates accept() methods
altogether, with only two lines of code, using new reflection
capabilities.
The Problem with Acceptance The
Visitor pattern seeks to add functionality to some arbitrary set of
classes by factoring out the new functionality into a visitor class.
The classes being enhanced by the visitor are known as elements.
In all of the variations of the Visitor
pattern, one mechanism or another is used to traverse some set of
element objects, visiting each one. However, to do this in the past,
each element had to define an accept() method. This method would take a
Visitor object as a parameter and call back that Visitor's visit()
method, passing itself as a parameter. A typical accept() method would
look like this:
public void accept(Visitor v) { v.visit(this); }
and simple traversal code might look like:
Visitor v = new Visitor(); for (int i=0; i<elementList.length; ++i)
elementList[i].accept(v);
It would be simpler and cleaner if the
element classes could remain unaware of the visitations. This is often
not just a matter of elegance. When working with external libraries or
frameworks, where you don't have control over the source code, accept()
methods can't just be added to the classes you would like to visit.
Usually this problem is addressed by counseling the developer to create
subclasses of these unchangeable element classes. The subclasses can
add the needed accept() method. However, this approach still has
problems; you usually can't change those same libraries and frameworks
to use your new subclass constructors when they instantiate the
elements you want to visit.
So, why is accept() used at all? Since most OO languages support what Design Patterns
calls single-dispatch method calls, and the Visitor pattern needs
double-dispatch method calls, the accept() method is the traditional
way to simulate a double-dispatch capability. In other words, the
version of the visit() method invoked should depend on both the class
of the visitor and the class of each element being visited.
However, since the Visitor class must add
a version of visit() for each element class that it wishes to visit
anyway, why not just call visit() directly? Why not pass visit() the
element as follows?
for (int i=0; i<elementList.length; ++i)
v.visit(elementList[i]);
While this would work when all the elements
are the same class, it won't handle the common situation where
elementList holds not simply Element objects, but a mix of objects that
are subclasses of Element. Or with the most general case in Java,
imagine elementList is an array of Object. The actual class of each
element could be anything. In that case, no matter what the real class
of each element object was, the method with the visit(Object) signature
would be invoked.
The Magic of Reflection In Java
1.2, some new reflection capabilities were added. The new getMethod()
call allows you to determine, at runtime, the method of a class that
has parameters with specified types. Another new method, invoke(), lets
you execute the methods found with getMethod().
A program that uses these new capabilities is shown in Listing 1,
which demonstrates a Visitor that doesn't need accept() methods. This
program defines three arbitrary element classes (Circle, Square, and
Beagle) that we wish to enhance with a Visitor. To keep things brief,
MyVisitor doesn't enhance these classes much, but it does follow the
Visitor pattern by defining a visit() method for each one. It also adds
a generic visit() method that has Object as its argument type. The
magic two lines of code in this method use the reflection API to
determine the real class of the element parameter. The appropriate
visit() method for the element's actual class is found and invoked.
Finally, a TestBed main program is defined. It instantiates elements of
various classes and collects them into an Object array. It then creates
a visitor object and invokes it while traversing the element
collection.
This technique can be used with any
collection of element classes, and with any type of collection
traversal or iteration technique. Also, the traversal can be managed by
the element collection, the visitor, or a third party, without
restriction. The only condition for using this technique is that you
must use Java 2 for your Visitor objects. But since the elements
themselves don't need to be modified, they don't have to be Java 2
objects.
Bruce Wallace is president of PolyGlot, Inc., a
software development consultancy. He
can be reached at bruce.wallace@acm.org.
|