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 --> VisitorWhen 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
instanceofchain.
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.
Click Run TS to execute. First run downloads Babel (~400 KB, cached after that).
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).
Click Run Go to execute. Runs via the Go Playground API.
Tradeoffs
| Pro | Con |
|---|---|
| New operations require a new class only, not changes to element classes | Adding a new element type requires updating every existing visitor |
| All logic for one operation lives in one class, not scattered across the hierarchy | accept boilerplate on every element class can feel ceremonial |
| Visitors can accumulate state across the entire traversal | Breaks encapsulation: elements must expose enough data for every possible visitor |
| Works naturally with Composite trees for recursive traversal | Double dispatch is non-obvious and surprises developers unfamiliar with the pattern |
| Operations are independently testable without constructing the full element hierarchy | Visitor 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
visitmethod. Statically typed languages catch the omission at compile time. Python and JavaScript do not, so a missing method causes a runtimeAttributeErrororTypeErrordeep inside a traversal. - Double dispatch is the mechanism, not the goal.
acceptcalls the visitor, the visitor calls the right method for the concrete type. If you flatten this tovisitor.visit(node)with anisinstancechain 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.singledispatchis a viable alternative to theacceptprotocol: register a handler for each concrete type and callvisit(node)without needingacceptat all. It is cleaner for a single module but loses the explicit interface contract thatProtocolor an ABC provides.
References
- Design Patterns: Visitor, GoF, the canonical definition
- Refactoring Guru: Visitor, diagrams and multiple-language examples
- SourceMaking: Visitor, additional context and known uses
Related topics
- 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