Applied Design Patterns with Java
Behavioral :: Visitor (331) {C ch 26}
Intent
Represent an operation to be performed on the elements of an
object structure. Visitor allows defining a new operation without changing the classes of the elements on which
it operates.
Motivation
Consider a compiler that represents programs as
abstract syntax trees. It will need to perform operations on abstract syntax trees for "static semantic"
analyses like checking that all variables are defined. It will also need to generate code. So it might define operations
for type-checking, code optimization, flow analysis, checking for variables being assigned values before they're
used, and so on. Also, the abstract syntax trees could be used for pretty-printing, program restructuring, code
instrumentation, and computing various metrics of a program.
Most of these operations will need to treat nodes that represent assignment statements differently from nodes that
represent variables or arithmetic expressions. Hence there will be one class for assignment statements, another
for variable accesses, another for arithmetic expressions, etc. The set of node classes depends on the language
being compiled, of course, but it doesn't change much for a given language.
The diagram above shows part of the Node class hierarchy. The problem
here is that distributing all these operations across the various node classes leads to a system that's hard to
understand, maintain, and change. It will be confusing to have type-checking code mixed with pretty-printing code
or flow analysis code. Adding a new operation usually requires recompiling all of these classes. It would be better
if each new operation could be added separately, and also if the node classes were independent of the operations
that apply to them.
Both can be done by packaging related operations from each class in a separate object, called a Visitor, and
passing it to elements of the abstract syntax tree as it's traversed. When an element "accepts" the Visitor, it
sends a request to the Visitor that encodes the element's class. It also includes the element as an argument.
The Visitor
will then execute the operation for that element—the operation that used to be in the class of the element.
A compiler that didn't use Visitors might type-check a procedure by calling the TypeCheck operation on its abstract
syntax tree. Each of the nodes would implement TypeCheck by calling TypeCheck on its components. If the compiler
type-checked a procedure using Visitors, then it would create a TypeCheckingVisitor object and call the Accept operation
on the abstract syntax tree with that object as an argument. Each of the nodes would implement Accept by calling
back on the Visitor:
an assignment node calls VisitAssignment operation on the visitor, while a variable reference calls VisitVariableReference.
What used to be the TypeCheck operation in class AssignmentNode is now the VisitAssignment operation on TypeCheckingVisitor.
To make Visitors
work for more than just type-checking, an abstract parent class NodeVisitor is needed for all Visitors of an abstract
syntax tree. NodeVisitor must declare an operation for each node class. An application that needs to compute program
metrics will define new subclasses of NodeVisitor and will no longer need to add application-specific code to the
node classes. The Visitor pattern encapsulates the operations for each compilation phase in a Visitor associated with
that phase.
With the Visitor pattern, define two class hierarchies: one for the elements being operated on (the Node hierarchy) and one for the Visitors that define operations on the elements (the NodeVisitor hierarchy). Create a new operation by adding a new subclass to the Visitor class hierarchy. As long as the grammar that the compiler accepts doesn't change (there is no need to add new Node subclasses), new functionality can be added simply by defining new NodeVisitor subclasses.