Skip to content

Visitor Pattern

The problem

A document AST holds Heading, Paragraph, and Image nodes. You need to export it to HTML, count words, check accessibility, and validate links. The straightforward approach is to add an exportHTML() method, a countWords() method, and so on to each node class. Every new operation touches every node class. The node classes accumulate unrelated logic, become hard to test in isolation, and have to change for reasons that have nothing to do with their core responsibility.

Visitor turns this inside out. Each node class gets a single accept(visitor) method that calls back to the visitor with itself. The visitor holds all the operation logic for every element type in one place. Adding a new operation means writing a new visitor class, not modifying the node classes. The node hierarchy stays closed to modification and the operations stay open to extension.

Structure

classDiagram
class Visitor {
<<interface>>
+visitHeading(node: Heading)
+visitParagraph(node: Paragraph)
+visitImage(node: Image)
}
class WordCountVisitor {
+count: number
+visitHeading(node: Heading)
+visitParagraph(node: Paragraph)
+visitImage(node: Image)
}
class HTMLExportVisitor {
+output: string
+visitHeading(node: Heading)
+visitParagraph(node: Paragraph)
+visitImage(node: Image)
}
class Node {
<<interface>>
+accept(visitor: Visitor)
}
class Heading {
+level: number
+text: string
+accept(visitor: Visitor)
}
class Paragraph {
+text: string
+accept(visitor: Visitor)
}
class Image {
+src: string
+alt: string
+accept(visitor: Visitor)
}
Visitor <|-- WordCountVisitor
Visitor <|-- HTMLExportVisitor
Node <|-- Heading
Node <|-- Paragraph
Node <|-- Image
Heading --> Visitor
Paragraph --> Visitor
Image --> Visitor

When to use

  • You need to perform many unrelated operations on an object structure and do not want to pollute the element classes with those operations.
  • The object structure is stable (element classes rarely change) but new operations are added frequently.
  • You want to gather related behavior in one class rather than scattering it across an entire hierarchy.
  • You are traversing a tree or graph and need to apply different algorithms at each node type without a large instanceof chain.

Implementation

Each node calls the appropriate visitor method inside accept, passing itself as the argument so the visitor receives the concrete type with no casting needed. Both visitors traverse the same tree without touching the node classes. Python does not enforce double dispatch the way statically typed languages do; functools.singledispatch is a compelling alternative that skips accept entirely and dispatches by type. In Go, interfaces are satisfied implicitly, and adding a new visitor requires no changes to the node structs.

idle
Click Run TS to execute. First run downloads Babel (~400 KB, cached after that).

Tradeoffs

ProCon
New operations require a new class only, not changes to element classesAdding a new element type requires updating every existing visitor
All logic for one operation lives in one class, not scattered across the hierarchyaccept boilerplate on every element class can feel ceremonial
Visitors can accumulate state across the entire traversalBreaks encapsulation: elements must expose enough data for every possible visitor
Works naturally with Composite trees for recursive traversalDouble dispatch is non-obvious and surprises developers unfamiliar with the pattern
Operations are independently testable without constructing the full element hierarchyVisitor and element hierarchies must stay in sync; a forgotten visit method is a silent bug in dynamic languages

Gotchas

  • The classic pain point: when you add a new element type, every visitor must add a corresponding visit method. Statically typed languages catch the omission at compile time. Python and JavaScript do not, so a missing method causes a runtime AttributeError or TypeError deep inside a traversal.
  • Double dispatch is the mechanism, not the goal. accept calls the visitor, the visitor calls the right method for the concrete type. If you flatten this to visitor.visit(node) with an isinstance chain inside, you have lost the point and added fragility.
  • Visitors that accumulate state (like WordCountVisitor) are not thread-safe by default. Create a fresh visitor per traversal or guard shared state explicitly.
  • Visitor pairs poorly with a volatile element hierarchy. If element classes change every sprint, the maintenance cost of updating every visitor outweighs the benefit. Prefer Strategy or simple method dispatch when the hierarchy is young.
  • In Python, functools.singledispatch is a viable alternative to the accept protocol: register a handler for each concrete type and call visit(node) without needing accept at all. It is cleaner for a single module but loses the explicit interface contract that Protocol or an ABC provides.

References

  • Design Patterns, the full GoF catalog
  • Composite, Visitor frequently traverses Composite trees
  • Iterator, often used together with Visitor to traverse collections
  • Strategy, also externalizes behavior but modifies the algorithm rather than the operation on elements