Home Design Patterns Visitor
Visitor
Behavioral Gang of Four“Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.”
Gamma, Helm, Johnson & Vlissides, p. 331Purpose
Visitor moves an operation out of the classes it operates on. Instead of every
node type in a hierarchy carrying a method for every thing anyone wants to do
with it, each node type carries one method, accept, and every
operation becomes a separate object with one method per node type.
What you buy is the ability to add an operation without editing any of the element classes. What you pay is that adding an element class now means editing every operation. That exchange is not a side effect of the pattern; it is the pattern. Visitor is a decision about which of the two directions of change you would rather have hurt.
Design
The mechanism is double dispatch. A single virtual call can select behavior
on one type; here you need it selected on two, the node and the operation, and no
mainstream object-oriented language does that in one step. So the pattern does it
in two. The client calls accept on a node, which dispatches on the
node's type, and the node calls the matching visit method on the
visitor, which dispatches on the visitor's type. Neither call alone is enough;
together they identify the pair.
Someone has to walk the structure, and the pattern does not say who. If the
elements do it, in accept, then traversal is written once and no
visitor can control the order or prune a branch. If the visitor does it, every
visitor repeats the walk and they get to disagree about it. A separate traversal
object is the third answer and the one that scales, at the cost of yet another
type in a pattern that already has plenty.
Example
Autobot Robotics stores an arm program as a tree: a program contains sequences, and sequences contain moves, waits, tool changes and IO operations. The office-side reporting system kept needing new things to do with that tree. Estimate the cycle time. Produce a printed listing an operator can follow. Validate the program against the machine's reach and speed limits. Export it to a customer's manufacturing system. Count tool changes for a maintenance forecast.
Each of those had been added as a method on every node class, so the node classes had grown a dozen methods each and had acquired dependencies on the report formatter and the export schema. Worse, the node types are shared with the embedded controller, so the reporting team's changes landed in a module that the controls team owned and had to review. Two teams were serialized on each other for work that had nothing to do with the other team's concerns.
The node classes now have accept and their own data, and nothing
else. Each operation is a visitor living in the reporting codebase. The bill came
due about a year later, when the arms gained a force-controlled insert move: the
new node type meant editing every one of the five visitors. The compiler listed
all five, which is the good version of that problem, but it was still five files
changed for one new node. It was the right trade because operations changed most
months and node types changed roughly twice a year. If that ratio had been
reversed, Visitor would have been a straightforwardly bad decision.
interface ProgramVisitor
{
void visitMove(MoveNode n);
void visitWait(WaitNode n);
void visitToolChange(ToolChangeNode n);
}
abstract class ProgramNode
{
public abstract void accept(ProgramVisitor v);
}
class MoveNode : ProgramNode
{
public double distance;
public double speed;
// Dispatch one: this call already knows it is a MoveNode.
// Dispatch two: the overload resolves against the visitor.
public override void accept(ProgramVisitor v) { v.visitMove(this); }
}
class WaitNode : ProgramNode
{
public double seconds;
public override void accept(ProgramVisitor v) { v.visitWait(this); }
}
class SequenceNode : ProgramNode
{
public ProgramNode[] children;
public override void accept(ProgramVisitor v)
{
foreach (ProgramNode c in children)
c.accept(v);
}
}
class CycleTimeVisitor : ProgramVisitor
{
public double total;
public void visitMove(MoveNode n) { total += n.distance / n.speed; }
public void visitWait(WaitNode n) { total += n.seconds; }
public void visitToolChange(ToolChangeNode n) { /* add swap time */ }
}
accept(ProgramVisitor v) and the v.visitMove(this)
inside it. That this is the second dispatch: it is typed as
MoveNode at that point, which is the only reason the right
overload gets picked.Seen in the Wild
Compilers and anything else that owns a syntax tree are where this pattern
actually lives. .NET's System.Linq.Expressions.ExpressionVisitor
exists so that LINQ providers can rewrite an expression tree without the tree
node types knowing anything about SQL. Roslyn ships
CSharpSyntaxVisitor and CSharpSyntaxWalker for the same
reason, and Clang's RecursiveASTVisitor is the C++ equivalent.
ANTLR generates a visitor interface for every grammar you give it. Outside
compilers, java.nio.file.FileVisitor, used by
Files.walkFileTree, is a genuine Visitor over a directory tree, and
Java's annotation processing API exposes ElementVisitor over the
program elements it hands you.
Comparisons
- Composite is usually the structure a visitor walks. The two are close to a matched pair: Composite gives you a tree of heterogeneous nodes behind one interface, and Visitor is how you add operations to that tree without swelling the node classes.
- Iterator answers the other half of the same question. An iterator decides the order in which elements are reached; a visitor decides what happens when one is reached. They compose well, and a design that uses both is usually clearer than a visitor that also walks.
- Interpreter builds exactly the kind of stable, closed node hierarchy that Visitor was designed for, which is why the two are so often described together. Once a grammar is fixed, every new thing you want to do with a parse tree is a visitor.
- Extension Object is the alternative when the element side is the part that keeps changing. Instead of every operation knowing every type, an element is asked at run time whether it supports an interface, which keeps the hierarchy open at the cost of the compile-time checking Visitor gives you.
- Strategy is what people often actually needed. If there is only one element type, a visitor is a strategy written with three times the machinery.
Why It Exists
My take
Visitor exists because of a genuine asymmetry in object-oriented languages, the one usually called the expression problem. A program that works over a set of types and a set of operations can be organized by either. Organize by type, which is what classes do, and adding a type is one new file while adding an operation touches every existing file. Organize by operation, which is what a procedural program with a switch statement does, and the reverse holds. Visitor is how you get the procedural trade-off inside a language that only offers you the other one. It is not a way to have both. It is a way to choose which one hurts, and choosing deliberately is better than inheriting whichever your language happened to give you.
Stated that way, the applicability rule falls out on its own. Visitor is correct when the element hierarchy is stable and closed and the operations are open-ended. An abstract syntax tree for a language whose grammar is fixed, a document model, a message format defined by a standard: those earn it. A domain model that is still being discovered does not, and applying Visitor there is actively harmful, because you have optimized for the axis that is not moving and taxed the one that is. Most of the bad Visitors I have seen were not badly written, they were correctly written for a situation that did not exist.
I would also say plainly that in a language with pattern matching over a
closed hierarchy you should not write this pattern at all. A sealed interface
with a switch that the compiler checks for exhaustiveness gives you the same
guarantee Visitor gives you, in one readable block, with no
accept methods and no double dispatch to explain. C# has that on
sealed record hierarchies, modern Java has it on sealed interfaces, and the
functional languages have had it for decades. Visitor is the workaround for a
language feature. Where the feature exists, use it.
Criticisms
The headline cost is the one everyone quotes and it deserves the attention: adding an element type breaks every visitor. In a codebase with twenty visitors, one new node is twenty edits, and if the visitor interface is published then some of those edits belong to people who do not work for you. Providing an abstract base visitor with empty defaults softens the compile error, and in doing so turns a build failure into a silent behavior change, where every existing visitor quietly ignores the new node type. Both options are bad; the pattern simply does not have a good answer here.
It also erodes the encapsulation it claims to respect. A visitor computing
cycle time needs the distance and the speed, so MoveNode must expose
them. Do that for five visitors with five different appetites and the node
classes end up with public fields or an accessor for everything they hold, which
is the opposite of what moving the operations out was supposed to achieve. The
intent statement says "without changing the classes of the elements", and in
practice you change them once, thoroughly, to make them readable from outside.
And it is hard to read. Double dispatch is not intuitive, the control flow bounces between two hierarchies, and a stack trace goes element, visitor, element, visitor all the way down. State accumulated in the visitor makes it single-use and not reentrant, which surprises people who try to run one over two trees. Of all the patterns in the book, this is the one I would most want a comment above, and the one where I would most want to be sure it was necessary before writing it.