cv.cm

Coding · Design patterns

Visitor pattern explained with examples

6 min read

Short answer

Visitor is a behavioral design pattern that lets you add new operations to a group of related classes without changing those classes. Each operation is written as a visitor object with one method per element type, and each element has an accept(visitor) method that calls the matching visitor method. This technique, called double dispatch, picks the code to run based on both the element type and the visitor type. Compilers, linters and code transformers use visitors to walk syntax trees.

Intent and the problem it solves

Intent: 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. It is one of the behavioral patterns in the Gang of Four (GoF) book.

Suppose a drawing program has three shape classes: Circle, Rectangle and Group. Over time you need many operations on them: compute the total area, export to SVG, export to JSON, count the shapes, check for overlaps. If every operation becomes a method on every shape class, the shape classes grow with code that has nothing to do with being a shape, and every new operation touches all of them.

With Visitor, each operation becomes one class (an area visitor, an SVG visitor) that contains the code for every shape type. The shapes only need one generic method, accept. Adding an operation means writing one new visitor; the shape classes stay unchanged.

Structure and participants

RoleResponsibility
VisitorInterface with one visit method per concrete element type, such as visitCircle and visitRectangle
Concrete visitorImplements one operation for all element types and may collect a result
ElementInterface with an accept(visitor) method
Concrete elementImplements accept by calling the visit method for its own type, passing itself
Object structureA collection or tree of elements, such as a document or a syntax tree, that visitors walk over

Why accept is needed: in TypeScript, Java and C#, a method call chooses its implementation from the type of the object it is called on (single dispatch). The accept call dispatches on the element type, and the call inside it, such as visitor.visitCircle(this), dispatches on the visitor type. Together they form double dispatch.

TypeScript implementation

The visitor interface is generic in its return type, so one visitor can compute numbers and another can produce strings.

interface ShapeVisitor<R> {
  visitCircle(c: Circle): R;
  visitRectangle(r: Rectangle): R;
  visitGroup(g: Group): R;
}
interface Shape {
  accept<R>(visitor: ShapeVisitor<R>): R;
}

class Circle implements Shape {
  constructor(readonly radius: number) {}
  accept<R>(v: ShapeVisitor<R>) { return v.visitCircle(this); }
}
class Rectangle implements Shape {
  constructor(readonly width: number, readonly height: number) {}
  accept<R>(v: ShapeVisitor<R>) { return v.visitRectangle(this); }
}
class Group implements Shape {
  constructor(readonly children: Shape[]) {}
  accept<R>(v: ShapeVisitor<R>) { return v.visitGroup(this); }
}

class AreaVisitor implements ShapeVisitor<number> {
  visitCircle(c: Circle) { return Math.PI * c.radius ** 2; }
  visitRectangle(r: Rectangle) { return r.width * r.height; }
  visitGroup(g: Group): number {
    return g.children.reduce((sum, child) => sum + child.accept(this), 0);
  }
}

class SvgVisitor implements ShapeVisitor<string> {
  visitCircle(c: Circle) { return '<circle r="' + c.radius + '"/>'; }
  visitRectangle(r: Rectangle) { return '<rect width="' + r.width + '" height="' + r.height + '"/>'; }
  visitGroup(g: Group): string { return "<g>" + g.children.map((c) => c.accept(this)).join("") + "</g>"; }
}

const drawing = new Group([new Circle(1), new Rectangle(2, 3), new Group([new Rectangle(1, 1)])]);
console.log(drawing.accept(new AreaVisitor()).toFixed(2)); // 10.14
console.log(drawing.accept(new SvgVisitor()));
// <g><circle r="1"/><rect width="2" height="3"/><g><rect width="1" height="1"/></g></g>

A third operation, such as counting shapes, would be a new class implementing ShapeVisitor<number>. None of the shape classes would change.

The TypeScript alternative: union types and switch

In TypeScript, a discriminated union with an exhaustive switch gives many of the same benefits with less code. The compiler reports an error if a case is missing, which replaces the safety that the visitor interface provides:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number }
  | { kind: "group"; children: Shape[] };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "rect": return s.width * s.height;
    case "group": return s.children.reduce((sum, c) => sum + area(c), 0);
    default: {
      const unreachable: never = s; // compile error if a kind is not handled
      return unreachable;
    }
  }
}

console.log(area({ kind: "group", children: [{ kind: "rect", width: 2, height: 3 }] })); // 6

Each new operation is a new function. This style fits plain data, such as JSON or syntax trees, and is what many TypeScript code bases use instead of class-based visitors.

When to use it and when not to

  • Use it when you have a stable set of element classes and need many unrelated operations on them, such as evaluation, printing, type checking and optimization on a syntax tree.
  • Use it to keep operation code together: all of the SVG export lives in one visitor, not spread over many classes.
  • Use it when a visitor needs to accumulate state while walking a structure, such as a symbol table or a running total.
  • Do not use it when element types change often; every change ripples through all visitors.
  • Do not use it when the elements are few and the operations are simple. Methods on the classes, or a function with a switch, are easier to read.
  • Be aware that visitors usually need access to element data, which can push you to make fields public.

Real-world uses

  • Babel plugins are visitors: an object with methods named after node types, such as Identifier or CallExpression, which @babel/traverse calls while walking the syntax tree.
  • ESLint rules return an object of visitor methods keyed by node type, and ESLint calls them during traversal.
  • Python's ast.NodeVisitor and ast.NodeTransformer classes call visit_ methods named after the node class, such as visit_Name.
  • Java's java.nio.file.FileVisitor, used with Files.walkFileTree, receives callbacks for each directory and file in a file tree.

Related patterns

PatternHow it relates to Visitor
CompositeVisitors are often applied to Composite structures; a group's visit method visits its children, as in the example.
InterpreterAn interpreter evaluates a syntax tree; a visitor can implement that evaluation, plus other operations on the same tree.
IteratorAn iterator walks the elements; a visitor defines the operation on each element. They can be combined.
StrategyA strategy varies one algorithm for one context; a visitor bundles one operation for many element types.

FAQ

What is double dispatch?

Double dispatch means the code that runs depends on the types of two objects. In Visitor, element.accept(visitor) selects the element type, and visitor.visitX(this) inside it selects the visitor type.

When should I not use the Visitor pattern?

Avoid it when new element types are added often, because each new type requires changes in every visitor. It also adds ceremony that is not worth it for a few simple operations.

Is Visitor useful in TypeScript?

It is useful for class-based object structures. For plain data, a discriminated union with an exhaustive switch usually achieves the same result with less code and full type checking.

Why do Babel and ESLint use visitors?

Both work on syntax trees with many node types and support many independent plugins or rules. Visitors let each plugin or rule handle only the node types it cares about, while the tool handles the traversal.

All coding tutorials