LESSON 7.5
Visitor
Represent an operation to be performed on the elements of an object structure without changing the classes of the elements on which it operates.
15 min read
Real-world analogy
A government tax auditor visiting different financial entities illustrates the Visitor pattern. A software contractor, a corporate enterprise, and a charitable foundation do not maintain complex tax calculation algorithms inside their own company classes. Instead, they implement an accept(Auditor a) method. When the tax auditor visits, each entity calls auditor.visitContractor(this) or auditor.visitCharity(this). The auditor executes the specific tax assessment formulas externally, leaving the corporate entities focused purely on their business operations.
The problem
You are building a rich text document processing engine. A document consists of an Abstract Syntax Tree (AST) composed of different node elements: Paragraph, Heading, CodeSnippet, and ImageElement.
Initially, you need to render the document to HTML. Soon, product managers request:
- Export to Markdown.
- Export to Plain Text.
- Spell-checking and grammar linting.
- Word count and reading time statistics calculation.
If you add each new operation directly as a method inside every AST node class, the node classes become bloated with unrelated formatting and parsing concerns:
// Naive implementation: Polluting AST elements with every operational concern
class Paragraph {
constructor(text) { this.text = text; }
// Polluted with export, linting, and metrics code
toHtml() { return `<p>${this.text}</p>`; }
toMarkdown() { return `${this.text}\n\n`; }
spellCheck(dictionary) { /* 50 lines of spell-checking logic */ }
calculateWordCount() { return this.text.split(" ").length; }
}
class CodeSnippet {
constructor(code, lang) { this.code = code; this.lang = lang; }
toHtml() { return `<pre><code>${this.code}</code></pre>`; }
toMarkdown() { return `\`\`\`${this.lang}\n${this.code}\n\`\`\`\n\n`; }
spellCheck() { /* Skip code blocks */ }
calculateWordCount() { return 0; }
}public class Example {
static class Paragraph {
String text;
public String toHtml() { return "<p>" + text + "</p>"; }
public String toMarkdown() { return text + "\n\n"; }
public void spellCheck() { /* Spell check logic */ }
public int countWords() { return text.split(" ").length
Adding a sixth operation (e.g. toPdf() or securitySanitize()) forces you to modify every single element class in the AST, violating the Single Responsibility Principle and the Open/Closed Principle.
The solution
The Visitor pattern extracts operations out of the element classes into separate Visitor classes. The element classes define a single accept(Visitor v) method. When called, the element passes itself to the visitor's corresponding visit method (v.visitParagraph(this)).
- Visitor (
DocumentVisitor): Interface declaring a visit method for every concrete element type (visitHeading,visitParagraph,visitCodeSnippet). - Concrete Visitors (
MarkdownExportVisitor,WordCountVisitor): Implements the specific operation across all element types. - Element (
DocumentElement): Declares theaccept(DocumentVisitor v)method. - Concrete Elements (
Paragraph,Heading,CodeSnippet): Implementsaccept()via Double Dispatch (v.visit(this)).
Step-by-step implementation
Step 1: Define the visitor interface
The visitor declares a dedicated visit method for every concrete element in the structure.
class DocumentVisitor {
visitHeading(heading) {
throw new Error("visitHeading() must be implemented");
}
visitParagraph(paragraph) {
throw new Error("visitParagraph() must be implemented");
}
}public class Example {
interface
Step 2: Define elements and double dispatch accept()
Elements implement accept(Visitor v) and pass this to the visitor.
Step 3: Implement concrete visitors
Visitors implement the operations across all elements in one encapsulated class.
The complete example
Here is the complete, runnable implementation demonstrating Markdown export and Word Count visitors operating on an AST document structure.
--- Rendered Markdown Output ---
# Design Patterns Overview
The Visitor pattern enables adding operations without mutating existing node classes.
```js
element.accept(visitor);
```
--- Calculated Word Count ---
Total prose words: 14Variations
Double dispatch explained
Why does the Visitor pattern require the seemingly convoluted element.accept(visitor) instead of calling visitor.visit(element) directly?
- In single-dispatch languages (Java, C++, C#), method dispatch depends only on the runtime type of the receiver (
obj.method()), but the compile-time static type of method arguments. - If you have
DocElement el = new Heading(...), callingvisitor.visit(el)causes a compiler error in Java because the compiler only knowselis aDocElement, not which specific overloadedvisit(Heading)method to call. - Double Dispatch solves this:
- First dispatch:
el.accept(visitor)invokes the runtime method onHeading. - Second dispatch: inside
Heading.accept(),visitor.visitHeading(this)knows with 100% compile-time certainty thatthisis aHeading.
- First dispatch:
Adding operations vs adding types
Visitor creates an asymmetric architectural trade-off:
- Adding Operations is Easy: To add a
PdfExportVisitor, you simply create one new class. No existing AST node classes are touched. - Adding Types is Hard: If you add a new
TableElementclass to the AST, you must update theVisitorinterface and addvisitTable(TableElement)to every single existing visitor in the codebase.
Modern alternatives (Java 21 sealed types & pattern matching)
In modern Java 21+, sealed interfaces and pattern-matching switch statements often replace the Visitor pattern:
// Java 21+ alternative without Double Dispatch:
sealed interface DocElement permits Heading, Paragraph, CodeBlock {}
public String renderMarkdown(DocElement el) {
return switch (el) {
case Heading h -> "#".repeat(h.level()) + " " + h.text();
case Paragraph p -> p.text();
The compiler verifies pattern exhaustiveness, achieving the benefits of Visitor with none of the double dispatch boilerplate.
Where you've already used it
- Babel & ESLint AST Visitors: Plugins define visitor hooks (
Identifier(node),FunctionDeclaration(node)) to lint and transform JavaScript ASTs. - Java Compiler API (
javax.lang.model.element.ElementVisitor): Java compiler annotation processors use visitors to scan classes, fields, and annotations. - ANTLR Parse Trees: Parsers generate parse-tree visitors allowing developers to traverse grammar nodes without modifying grammar definitions.
Pros and cons
Advantages
- Open/Closed Principle for Operations: Add new operations (export, lint, audit) across an object structure without modifying node classes.
- Single Responsibility Principle: Keeps related operations together in a single visitor class rather than scattered across 20 element files.
- Accumulator State: A visitor can maintain state (such as total word count or indentation level) as it traverses nodes.
Disadvantages
- Breaks Open/Closed for Types: Adding a new element type requires modifying every existing visitor.
- Breaks Encapsulation: Visitors often require elements to expose public getters for private fields they need to inspect.
- Double Dispatch Complexity: Cognitive overhead of tracing execution across two layers of dispatching.
When not to use it
Do not use Visitor if the hierarchy of element classes is constantly changing and new element classes are added frequently. In that scenario, the maintenance burden of updating all visitors outweighs the benefits.
Related patterns
| Pattern | Focus | Mechanism |
|---|---|---|
| Visitor | Adds operations to heterogeneous hierarchies | Double dispatch |
| Composite | Represents part-whole trees | Visitors often traverse Composite trees |
| Iterator | Sequential element access | Iterators can drive Visitor traversal |
| Strategy | Swapping algorithms on a single context | Composition without double dispatch |
Visitor: document AST operations
Externalize operations like HTML rendering and word counting from document AST nodes into double-dispatching Visitor classes.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
Why is Double Dispatch required in the Visitor pattern?
Because languages like Java use single dynamic dispatch (based on receiver runtime type, but argument compile-time type). Calling element.accept(v) ensures the first dispatch selects the concrete element's method, which then executes the second dispatch calling v.visit(this) with its exact concrete type.
What is the primary drawback of the Visitor pattern?
Adding a new element type to the hierarchy breaks all existing visitor classes, requiring every visitor to implement a new visitNewElement() method.
How does Java 21's pattern matching switch replace the Visitor pattern?
Sealed interfaces define a finite set of subtypes. A pattern-matching switch expression allows external functions to match against all concrete types exhaustively, providing compiler safety without double dispatch boilerplate.
Can a Visitor maintain state during traversal? Yes. Unlike element methods, a Visitor instance is stateful across the traversal, allowing it to aggregate totals (like word counts, total prices, or syntax error lists).
Try it
Arithmetic Expression Evaluator LLD
Tokenize and parse arithmetic with precedence, unary operators and bounded evaluation.