Recall our definition for an arithmetic expression without variables:
ArithExpr := Const(int) + Sum(ArithExpr, ArithExpr) + ...
Our implementation of this data definition using the composite pattern would be more robust and more flexible if we could define new operations on ArithExprs without modifying any existing code. Fortunately, there is a clever design pattern called the visitor pattern that lets us do this. The idea underlying the visitor pattern is to bundle the method definitions for a new operation together in a new class called a visitor class. In the composite pattern, each operation o defined over the composite class (e.g., ArithExpr) must have a separate method definition in each subclass of the composite class (e.g.,Const, Sum, ...). We invoke the methods in a visitor object by including an abstract hook method called apply to the top class (e.g. ArithExpr) of the hierarchy and appropriately overriding it in each concrete variant.
Let's illustrate the visitor design pattern by defining a visitor class that evaluates arithmetic expressions.
First, we define an interface Visitor that specifies what methods must be included in every visitor class for ArithExpr:
interface Visitor {
int forConst(Const c);
int forSum(Sum s);
int forNeg(Neg n);
}
Notice that the class includes exactly one method for each subclass or
ArithExpr. Each method takes an instance of the particular
subclass that it processes. This argument, called the host, is
needed to give the visitor method access to all the information that
would be available through this if the method were defined
by a conventional method definitions inside each subclass of the composite
hierarchy. For example, the method code may need to access the
fields of the host object using by accessors.
Second, we add an abstract method
to the abstract class ArithExpr. For each concrete variant in the composite class hierarchy, the apply method that selects the corresponding method from the input Visitor v and passes this to it. For example, the apply method is defined in the Const and Sum classes as follows:/** invoke the appropriate method from v on this host */ abstract int apply(Visitor v);
class Const extends ArithExpr {
...
public int apply(Visitor v) { return v.forConst(this); }
...
}
class Sum {
...
public int apply(Visitor v) { return v.forSum(this); }
}
To implement the evaluator for ArithExpr, we create a concrete visitor class EvalVisitor implementing the Visitor interface. Each method in the class specifies how to process a particular concrete variant of the composite hierarchy.
class EvalVisitor implements Visitor {
public int forConst(Const c) {
return c.getValue();
}
public int forSum(Sum s) {
return s.getLeft().apply(this) + s.getRight().apply(this);
}
public int forNeg(Neg n) {
return -(n.getArg().apply(this));
}
}
To evaluate an arithmetic expression, we simply call
a.apply(new EvalVisitor())If we wish to define more operations on arithmetic expressions, we can define new visitor classes to hold the methods, but there is no need to modify any of the subclasses of ArithExpr.
Applying the Singleton Pattern. Since a visitor has no fields, all instances of a particular visitor class are identical. So it is wasteful to create new instances of the visitor every time we wish to pass it to an apply method. We can eliminate this waste by using the singleton design pattern. Recall that this pattern places a static field in the singleton class bound to the only instance of that class.
class EvalVisitor {
static EvalVisitor only = new EvalVisitor();
...
}
Then, instead of apply(new EvalVisitor()),we may simply write
apply(EvalVisitor.only).
Applying Anonymous Classes. An even simpler and more convenient way to define a visitor class is to use an anonymous class. Recall that an anonymous class has the following syntax:
new className( arg1, ..., argm) {In most cases, the class className is either an abstract class or an interface, but it can be any class. The argument list arg1, ..., argm is used to call the constructor for the class className; if className is an interface, the argument list must be empty. The member list}
For example, to create an instance of a visitor that evaluates an arithmetic expression, we write:
new Visitor() {
int forConst(Const c) {...}
int forSum(Sum s) {...}
...
}
Since we generally want to use a visitor more than once, we
usually bind the anonymous class instance
to a variable, so we can access it again! The statement:
Visitor ev = new Visitor() {
int forConst(Const c) {...};
int forSum(Sum s) {...};
...
};
binds the variable ev to our anonymous class instance.