The
“visitor” pattern
Now
consider applying a design pattern with an entirely different goal to the
trash-sorting problem.
For
this pattern, we are no longer concerned with optimizing the addition of new
types of
Trash
to
the system. Indeed, this pattern makes adding a new type of
Trash
more
complicated. The assumption is that you have a primary class hierarchy that is
fixed; perhaps it’s from another vendor and you can’t make changes
to that hierarchy. However, you’d like to add new polymorphic methods to
that hierarchy, which means that normally you’d have to add something to
the base class interface. So the dilemma is that you need to add methods to the
base class, but you can’t touch the base class. How do you get around this?
The
design pattern that solves this kind of problem is called a
“visitor” (the final one in the
Design
Patterns
book), and it builds on the double
dispatching
scheme shown in the last section.
The
visitor
pattern allows you to extend the interface of the primary type by creating a
separate class hierarchy of type
Visitor
to
virtualize the operations performed upon the primary type. The objects of the
primary type simply “accept” the visitor, then call the
visitor’s dynamically
-bound
method. It looks like this:
Now,
if
v
is a
Visitable
handle
to an
Aluminum
object, the code:
PriceVisitor
pv = new PriceVisitor();
v.accept(pv);
causes
two polymorphic method calls: the first one to select
Aluminum’s
version of
accept( ),
and the second one within
accept( )
when the specific version of
visit( )
is called dynamically using the base-class
Visitor
handle
v. This
configuration means that new functionality can be added to the system in the
form of new subclasses of
Visitor.
The
Trash
hierarchy
doesn’t need to be touched. This is the prime benefit of the visitor
pattern: you can add new polymorphic functionality to a class hierarchy without
touching that hierarchy (once the
accept( )
methods have been installed). Note that the benefit is helpful here but not
exactly what we started out to accomplish, so at first blush you might decide
that this isn’t the desired solution.
But
look at one thing that’s been accomplished: the visitor solution avoids
sorting from the master
Trash
sequence into individual typed sequences. Thus, you can leave everything in the
single master sequence and simply pass through that sequence using the
appropriate visitor to accomplish the goal. Although this behavior seems to be
a side effect of visitor, it does give us what we want (avoiding RTTI).
The
double
dispatching in the visitor pattern takes care of determining both the type of
Trash
and
the type of
Visitor.
In
the following example, there are two implementations of
Visitor:
PriceVisitor
to both determine and sum the price, and
WeightVisitor
to keep track of the weights.
You
can see all of this implemented in the new, improved version of the recycling
program. As with
DoubleDispatch.java,
the
Trash
class is left alone and a new interface is created to add the
accept( )
method:
//: Visitable.java
// An interface to add visitor functionality to
// the Trash hierarchy without modifying the
// base class.
package c16.trashvisitor;
import c16.trash.*;
interface Visitable {
// The new method:
void accept(Visitor v);
} ///:~
The
subtypes of
Aluminum,
Paper
,
Glass,
and
Cardboard
implement the
accept( )
method:
//: VAluminum.java
// Aluminum for the visitor pattern
package c16.trashvisitor;
import c16.trash.*;
public class VAluminum extends Aluminum
implements Visitable {
public VAluminum(double wt) { super(wt); }
public void accept(Visitor v) {
v.visit(this);
}
} ///:~
//: VPaper.java
// Paper for the visitor pattern
package c16.trashvisitor;
import c16.trash.*;
public class VPaper extends Paper
implements Visitable {
public VPaper(double wt) { super(wt); }
public void accept(Visitor v) {
v.visit(this);
}
} ///:~
//: VGlass.java
// Glass for the visitor pattern
package c16.trashvisitor;
import c16.trash.*;
public class VGlass extends Glass
implements Visitable {
public VGlass(double wt) { super(wt); }
public void accept(Visitor v) {
v.visit(this);
}
} ///:~
//: VCardboard.java
// Cardboard for the visitor pattern
package c16.trashvisitor;
import c16.trash.*;
public class VCardboard extends Cardboard
implements Visitable {
public VCardboard(double wt) { super(wt); }
public void accept(Visitor v) {
v.visit(this);
}
} ///:~
Since
there’s nothing concrete in the
Visitor
base class, it can be created as an
interface:
//: Visitor.java
// The base interface for visitors
package c16.trashvisitor;
import c16.trash.*;
interface Visitor {
void visit(VAluminum a);
void visit(VPaper p);
void visit(VGlass g);
void visit(VCardboard c);
} ///:~
Once
again custom
Trash
types have been created in a different subdirectory. The new
Trash
data file is
VTrash.dat
and looks like this:
c16.TrashVisitor.VGlass:54
c16.TrashVisitor.VPaper:22
c16.TrashVisitor.VPaper:11
c16.TrashVisitor.VGlass:17
c16.TrashVisitor.VAluminum:89
c16.TrashVisitor.VPaper:88
c16.TrashVisitor.VAluminum:76
c16.TrashVisitor.VCardboard:96
c16.TrashVisitor.VAluminum:25
c16.TrashVisitor.VAluminum:34
c16.TrashVisitor.VGlass:11
c16.TrashVisitor.VGlass:68
c16.TrashVisitor.VGlass:43
c16.TrashVisitor.VAluminum:27
c16.TrashVisitor.VCardboard:44
c16.TrashVisitor.VAluminum:18
c16.TrashVisitor.VPaper:91
c16.TrashVisitor.VGlass:63
c16.TrashVisitor.VGlass:50
c16.TrashVisitor.VGlass:80
c16.TrashVisitor.VAluminum:81
c16.TrashVisitor.VCardboard:12
c16.TrashVisitor.VGlass:12
c16.TrashVisitor.VGlass:54
c16.TrashVisitor.VAluminum:36
c16.TrashVisitor.VAluminum:93
c16.TrashVisitor.VGlass:93
c16.TrashVisitor.VPaper:80
c16.TrashVisitor.VGlass:36
c16.TrashVisitor.VGlass:12
c16.TrashVisitor.VGlass:60
c16.TrashVisitor.VPaper:66
c16.TrashVisitor.VAluminum:36
c16.TrashVisitor.VCardboard:22
The
rest of the program creates specific
Visitor
types and sends them through a single list of
Trash
objects:
//: TrashVisitor.java
// The "visitor" pattern
package c16.trashvisitor;
import c16.trash.*;
import java.util.*;
// Specific group of algorithms packaged
// in each implementation of Visitor:
class PriceVisitor implements Visitor {
private double alSum; // Aluminum
private double pSum; // Paper
private double gSum; // Glass
private double cSum; // Cardboard
public void visit(VAluminum al) {
double v = al.weight() * al.value();
System.out.println(
"value of Aluminum= " + v);
alSum += v;
}
public void visit(VPaper p) {
double v = p.weight() * p.value();
System.out.println(
"value of Paper= " + v);
pSum += v;
}
public void visit(VGlass g) {
double v = g.weight() * g.value();
System.out.println(
"value of Glass= " + v);
gSum += v;
}
public void visit(VCardboard c) {
double v = c.weight() * c.value();
System.out.println(
"value of Cardboard = " + v);
cSum += v;
}
void total() {
System.out.println(
"Total Aluminum: $" + alSum + "\n" +
"Total Paper: $" + pSum + "\n" +
"Total Glass: $" + gSum + "\n" +
"Total Cardboard: $" + cSum);
}
}
class WeightVisitor implements Visitor {
private double alSum; // Aluminum
private double pSum; // Paper
private double gSum; // Glass
private double cSum; // Cardboard
public void visit(VAluminum al) {
alSum += al.weight();
System.out.println("weight of Aluminum = "
+ al.weight());
}
public void visit(VPaper p) {
pSum += p.weight();
System.out.println("weight of Paper = "
+ p.weight());
}
public void visit(VGlass g) {
gSum += g.weight();
System.out.println("weight of Glass = "
+ g.weight());
}
public void visit(VCardboard c) {
cSum += c.weight();
System.out.println("weight of Cardboard = "
+ c.weight());
}
void total() {
System.out.println("Total weight Aluminum:"
+ alSum);
System.out.println("Total weight Paper:"
+ pSum);
System.out.println("Total weight Glass:"
+ gSum);
System.out.println("Total weight Cardboard:"
+ cSum);
}
}
public class TrashVisitor {
public static void main(String[] args) {
Vector bin = new Vector();
// ParseTrash still works, without changes:
ParseTrash.fillBin("VTrash.dat", bin);
// You could even iterate through
// a list of visitors!
PriceVisitor pv = new PriceVisitor();
WeightVisitor wv = new WeightVisitor();
Enumeration it = bin.elements();
while(it.hasMoreElements()) {
Visitable v = (Visitable)it.nextElement();
v.accept(pv);
v.accept(wv);
}
pv.total();
wv.total();
}
} ///:~
Note
that the shape of
main( )
has changed again. Now there’s only a single
Trash
bin. The two
Visitor
objects are accepted into every element in the sequence, and they perform their
operations. The visitors keep their own internal data to tally the total
weights and prices.
Finally,
there’s no run-time type identification other than the inevitable cast to
Trash
when pulling things out of the sequence. This, too, could be eliminated with
the implementation of parameterized types in Java.
One
way you can distinguish this solution from the double dispatching solution
described previously is to note that, in the double dispatching solution, only
one of the overloaded methods,
add( ),
was overridden when each subclass was created, while here
each
one of the overloaded
visit( )
methods is overridden in every subclass of
Visitor.
More
coupling?
There’s
a lot more code here, and there’s definite coupling between the
Trash
hierarchy and the
Visitor
hierarchy. However, there’s also high cohesion within the respective sets
of classes: they each do only one thing (
Trash
describes
Trash, while
Visitor
describes
actions performed on
Trash),
which is an indicator of a good design. Of course, in this case it works well
only if you’re adding new
Visitors,
but it gets in the way when you add new types of
Trash. Low
coupling between classes and high cohesion within a class is definitely an
important design goal. Applied mindlessly, though, it can prevent you from
achieving a more elegant design. It seems that some classes inevitably have a
certain intimacy with each other. These often occur in pairs that could perhaps
be called couplets,
for example, collections and iterators (
Enumerations).
The
Trash-Visitor
pair above appears to be another such couplet.