Skip to main content

Patternadvanced

Visitor

Overview

Visitor separates an algorithm from the object structure it operates on, allowing new operations to be added without altering the structure's classes.

It is the pattern with the most ceremony in the catalogue: it requires an accept method on every type in the hierarchy, a visitor interface with one operation per type, and the double dispatch almost nobody reads without stopping to think. That ceremony is what makes it the case where the question "do I really need this?" pays off most. The reason lies in a single axis, and understanding it settles almost every decision about the pattern.

Problem

An object structure with several node types needs several different operations.

A syntax tree with numbers, operators and variables has to be evaluated, printed, optimized, type-checked and serialized.

Putting each operation on the nodes works — but each new operation touches every node class, and the classes accumulate unrelated responsibilities.

Visitor inverts that: each operation becomes an object that knows how to handle every node type.

Core Concepts

The expression problem

The formulation that settles everything. There are two axes of growth:

New types — more node types in the structure. New operations — more things to do with the structure.

Among the usual organizations of object-oriented code, none makes both cheap at the same time — it is the expression problem, stated by Wadler in 1998. There are encodings that buy both directions (object algebras, tagless final), at the cost of even more ceremony and of features not every language offers.

Method on the nodeVisitor
Adding a node typeCheap — one classExpensive — touches every visitor
Adding an operationExpensive — touches every nodeCheap — one class

Visitor favours operations and penalizes types. Applying it to a structure whose types grow is choosing the wrong side of the dilemma — and it is the most common mistake with this pattern.

Double dispatch

The mechanism. In languages with single dispatch, the method called depends on one type. Visitor needs two: the node's type and the operation's type.

node.accept(visitor) ← dispatches on the node's type
visitor.visitNumber(this) ← dispatches on the visitor's type

The accept on each node exists only for that. It is mandatory ceremony, and what makes the pattern verbose.

In languages with pattern matching over algebraic types, the same thing is written with no hierarchy and no accept — which explains why Visitor is rare in functional code.

The readability cost

An operation distributed across visitNumber, visitOperator, visitVariable is cohesive as a class and fragmented as reading: understanding what it does requires reading every method and mentally reconstructing the traversal.

When to Use

  • The type structure is stable and the operations grow.
  • The operations are complex and do not conceptually belong to the nodes.
  • An operation needs to accumulate state while traversing the structure.
  • Operations have to be applied to a hierarchy you do not control.

When Not to Use

When new types appear frequently. The wrong side of the dilemma. Each type touches every visitor.

When there is only one operation. All the ceremony with none of the benefit.

When the language has pattern matching. A function with match over the types does the same with a fraction of the code.

When the operation belongs to the node. If evaluate is what the node is, it should know how to evaluate itself.

When readability matters more than extensibility. Visitor fragments the reading of each operation, and in frequently read code that weighs.

Alternatives

  • Pattern matching — in languages that offer it, it is superior in almost every respect.
  • A method on the node — when the operations are few and stable.
  • Iterator with a type conditional — less elegant, far more readable, and appropriate when there are few types.
  • A dispatch table — a map from type to function, with no visitor hierarchy.

Trade-offs

VisitorMethod on the node
A new operation is one classTouches every node
A new type touches every visitorIs one class
Operation cohesive in one classDistributed across nodes
Reading fragmented by typeEach node readable in isolation
accept mandatory on each nodeNo ceremony
Accumulates state during traversalState has to be passed

Failure Modes

A new type forgotten in a visitor. It depends on the visitor's form: with an abstract interface, adding a type breaks the compilation of every one of them — expensive, and it is the cost the table above charges. With a base class providing default implementations, or in a dynamic language, nothing breaks: the new type falls into the default and the behaviour is wrong in silence.

Visitor with shared state. Reusing the instance across traversals produces contamination.

Hierarchy of visitors. A base visitor with default behaviour and subclasses that override: it combines Visitor's costs with inheritance's.

Implicit traversal order. Who controls the traversal — the node or the visitor — is not obvious, and changing it breaks visitors that depended on the order.

Common Mistakes

Applying it to a structure with unstable types. The central mistake.

Using it in a language with pattern matching.

Putting in the visitor what belongs to the node.

Not handling the unknown type case. A visitor that does not cover a type should fail explicitly, not ignore it.

Where it appears in practice

Compilers and interpreters. The canonical case and the one that best satisfies the condition: a language's grammar is stable for years, and the operations — type checking, optimization, code generation, formatting — grow.

Static analysis tools. Each rule is a visitor over the syntax tree. New rules are frequent; node types are not.

Serialization of complex structures. One visitor per format.

Traversal of structured documents. Transformations over document trees.

All four share the same characteristic, and it is the pattern's test: the structure was defined by an external specification — a grammar, a format, a standard — and so it does not change at the pace of the code. When the structure is yours and evolves with the domain, the condition does not hold.

Real-World Example

A business rules system represented conditions as a tree: comparisons, conjunctions, disjunctions, negations, field references.

Five node types, defined by the rules language's grammar — stable from the start.

The operations grew: evaluate, render for human reading, convert to SQL, estimate cost, extract referenced fields, validate.

Six visitors, each in a class. Adding an operation never touched the nodes.

The counterexample, in the same system: someone applied Visitor to the domain's document hierarchy — contract, policy, endorsement, claim. That hierarchy grew: in two years it gained five new types.

Each new type touched the four existing visitors. On two occasions a type was added and a visitor was not updated — and the defect got through because nothing enforced exhaustiveness.

It was reverted to methods on the documents themselves.

The same team, the same pattern, two opposite outcomes. The difference was which axis grew.

  • Composite — the structure Visitor usually operates on.
  • Iterator — traversal with no type distinction.
  • Strategy — algorithm variation with no structure.

Practical Exercise

Pick a type hierarchy in your system and answer from the history: how many new types were added in the last year? How many new operations?

If types > operations, Visitor is the wrong pattern there. If operations > types and the structure comes from an external specification, it may be worth it.

Interview Questions

  • What is the expression problem and which side does Visitor choose?
  • Why does Visitor need double dispatch?
  • In what kind of language does it stop being necessary?

Further Exploration

  • Gamma, Erich et al. Design Patterns. Addison-Wesley, 1994.
  • Wadler, Philip. The Expression Problem, 1998.
Finished reading this document?Your progress is saved in this browser only.