Visitor
Separates an algorithm from the object structure it operates on.
Visitor Pattern: A Complete Guide with Examples in 11 Programming Languages
The Visitor Pattern is a behavioral design pattern that lets you add new operations to a class hierarchy without modifying the classes themselves. Instead of cluttering each class with every operation it might ever need, you define a separate visitor object that contains the operation's logic. Each class in the hierarchy accepts a visitor and calls the visitor's method that corresponds to its own type. The class hierarchy stays untouched; new operations arrive as new visitor classes.
In this comprehensive guide, we'll explore the Visitor Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.
Table of Contents
- What is the Visitor Pattern?
- Why Use the Visitor Pattern?
- Visitor Pattern Comparison
- Visitor Pattern Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Visitor Pattern?
The Visitor Pattern solves a problem that appears whenever you have a stable class hierarchy — a fixed set of types that rarely changes — but you need to perform many different operations across all those types. The naive approach is to add every operation as a method to every class. A document model with Paragraph, Heading, Image, and Table nodes might end up with render(), exportToPdf(), countWords(), validate(), spellCheck(), and translate() all as methods on every node class. The nodes become bloated with logic that has nothing to do with their core responsibility of representing document structure.
The Visitor Pattern separates the data structure from the operations performed on it. Each class in the hierarchy keeps only one new method: accept(visitor). This method calls back to the visitor with the exact type of the object — visitor.visitParagraph(this), visitor.visitHeading(this), etc. The visitor object then handles the type-specific logic. To add a new operation, you add a new visitor class — the node classes never change.
Think of it like a document being audited by different inspectors. The document's pages and paragraphs don't change when a grammar checker arrives. They don't change when a word counter arrives, or a spell checker, or a PDF exporter. Each inspector (visitor) walks through the document (element hierarchy) doing their specific job. The document just accepts each inspector and shows them around.
Why Use the Visitor Pattern?
The Visitor Pattern offers several compelling benefits:
- Open/Closed Principle for Operations: Add new operations (visitors) without modifying any existing element class — new behavior is a new visitor class, not an edit to each node
- Single Responsibility: Each visitor class is responsible for exactly one operation across the hierarchy — rendering logic, export logic, and validation logic each live in their own class
- Accumulate State Across Operations: A visitor can collect results as it traverses the hierarchy — a
WordCountVisitoraccumulates a running total; aValidationVisitoraccumulates all errors found - Type-Specific Dispatch Without Downcasting: The
accept/visitmechanism provides double dispatch — the exact runtime type of the element determines whichvisit*()method is called, with noinstanceofchecks or casts - Cross-Cutting Operations: Operations that touch multiple unrelated classes can be unified in one visitor — all the logic for one feature lives together rather than scattered across the hierarchy
- Separates Concerns Cleanly: The element classes own the data and structure; visitor classes own the operations — neither knows about the other's implementation details
Visitor Pattern Comparison
Let's compare the Visitor Pattern with related patterns:
| Pattern | Purpose | Who Adds Behavior | Element Changes? | Use Case |
|---|---|---|---|---|
| Visitor | Add operations to a stable hierarchy | New Visitor class | No | Compilers, document processors, AST analysis |
| Strategy | Swap algorithms at runtime | Client injects strategy | No | Sorting, routing, payment algorithms |
| Command | Encapsulate a request as an object | New Command class | No | Undo/redo, job queues, transactions |
| Composite | Tree structures of objects | Modify composite | Yes (add accept) | UI trees, file systems, document nodes |
| Iterator | Sequential traversal of a collection | New Iterator | Minimal | List, tree, graph traversal |
Key Distinctions:
- Visitor vs. Strategy: Both add behavior without modifying existing classes, but they serve different purposes. Strategy swaps how a single operation is performed — the algorithm varies but there's one operation. Visitor adds new operations to a class hierarchy — the element types are fixed but the number of operations grows. Use Strategy when the variation is the algorithm; use Visitor when the variation is the operation performed on a family of types.
- Visitor vs. Iterator: Both traverse a structure. Iterator provides a standard way to step through elements sequentially — it doesn't care what it does with each element. Visitor carries the operation with it — it knows exactly what to do with each element type. In practice, a Visitor often uses an Iterator (or recursion) internally to walk the structure.
- Visitor vs. Decorator: Decorator wraps individual objects to add behavior transparently — the client doesn't know the wrapper is there. Visitor is explicitly accepted by the element — the element calls the visitor back. Visitor is better for bulk operations across a hierarchy; Decorator is better for per-object transparent augmentation.
- Visitor vs. Command: Both encapsulate operations as objects. Command encapsulates a single action (with undo). Visitor encapsulates an operation across a type hierarchy — it has a different method for each element type. A Visitor could contain multiple Commands, one per element type.
Visitor Pattern Explained
The Visitor Pattern involves four participants:
Core Components:
-
Visitor Interface: Declares a
visit*()method for each concrete element type in the hierarchy. Every operation that needs to be performed on the hierarchy is a separate visitor class implementing this interface. The interface is the "contract" that ties visitors to elements. -
Concrete Visitors: Each concrete visitor implements the
visit*()methods for all element types, providing the logic for one specific operation. AWordCountVisitorimplementsvisitParagraph(),visitHeading(),visitTable()— each counting words in the respective element type. APdfExportVisitorimplements the same methods but with PDF rendering logic. -
Element Interface: Declares the
accept(visitor)method that every element in the hierarchy must implement. This is the only change to the class hierarchy — one method per class. Theaccept()method is always identical in structure: call the visitor's method that corresponds to this element's type. -
Concrete Elements: Each element implements
accept(visitor)by calling the visitor's correspondingvisit*()method, passing itself as the argument. AParagraphimplementsaccept(visitor)asvisitor.visitParagraph(this). This is the double dispatch mechanism — the element's runtime type selects the rightvisit*()method.
Double Dispatch Explained:
Standard method calls in most object-oriented languages are single dispatch — the runtime type of the receiver selects the method. element.render() selects the render() implementation based on element's type.
The Visitor Pattern implements double dispatch — the method selected depends on the runtime types of two objects: the visitor and the element. Without language-level support for multiple dispatch, this is achieved manually via the accept/visit handshake:
element.accept(visitor)— first dispatch: selects based onelement's type- Inside
accept:visitor.visitX(this)— second dispatch: selects based onvisitor's type (which concrete visitor method is called)
The result: the correct visit*() method for the specific combination of visitor type and element type is invoked automatically, without any instanceof checks or type casting.
Traversal Strategies:
The Visitor Pattern says nothing about how to traverse the hierarchy. Traversal can be:
- Visitor-driven: The visitor decides the traversal order and calls
accept()on child elements from within thevisit*()methods - Element-driven: Each element's
accept()method first calls the visitor on itself, then callsaccept()on its children — recursive tree traversal built into the element - External traversal: A separate iterator or walker traverses the structure and calls
accept()on each element — maximum flexibility, decoupled from both visitors and elements
Class Diagram
Here's the UML class diagram showing the Visitor Pattern structure:
Beginner-Friendly Example
Let's start with the most intuitive Visitor example: a document node hierarchy that supports multiple operations. The document has Paragraph, Heading, Image, and Table nodes. We'll implement a WordCountVisitor that counts all words and an HtmlExportVisitor that renders the document to HTML. The node classes never change when we add new operations.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
# ── Visitor Interface ─────────────────────────────────────────────
class DocumentVisitor(ABC):
@abstractmethod
def visit_paragraph(self, paragraph: "Paragraph") -> None: ...
@abstractmethod
def visit_heading(self, heading: "Heading") -> None: ...
@abstractmethod
def visit_image(self, image: "Image") -> None: ...
@abstractmethod
def visit_table(self, table: "Table") -> None: ...
# ── Element Interface ─────────────────────────────────────────────
class DocumentElement(ABC):
@abstractmethod
def accept(self, visitor: DocumentVisitor) -> None: ...
# ── Concrete Elements ─────────────────────────────────────────────
class Paragraph(DocumentElement):
def __init__(self, text: str) -> None:
self.text = text
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_paragraph(self) # Double dispatch: routes to visitParagraph
class Heading(DocumentElement):
def __init__(self, text: str, level: int = 1) -> None:
self.text = text
self.level = level
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_heading(self)
class Image(DocumentElement):
def __init__(self, src: str, alt_text: str = "") -> None:
self.src = src
self.alt_text = alt_text
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_image(self)
class Table(DocumentElement):
def __init__(self, rows: List[List[str]]) -> None:
self.rows = rows # First row is treated as header
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_table(self)
# ── Concrete Visitors ─────────────────────────────────────────────
class WordCountVisitor(DocumentVisitor):
"""Counts all words across every element in the document."""
def __init__(self) -> None:
self._count = 0
@property
def count(self) -> int:
return self._count
def _count_words(self, text: str) -> int:
return len(text.split()) if text.strip() else 0
def visit_paragraph(self, paragraph: Paragraph) -> None:
words = self._count_words(paragraph.text)
self._count += words
print(f" [WordCount] Paragraph: {words} words")
def visit_heading(self, heading: Heading) -> None:
words = self._count_words(heading.text)
self._count += words
print(f" [WordCount] Heading h{heading.level}: {words} words")
def visit_image(self, image: Image) -> None:
words = self._count_words(image.alt_text)
self._count += words
print(f" [WordCount] Image alt text: {words} words")
def visit_table(self, table: Table) -> None:
words = sum(self._count_words(cell) for row in table.rows for cell in row)
self._count += words
print(f" [WordCount] Table: {words} words")
class HtmlExportVisitor(DocumentVisitor):
"""Renders all document elements as HTML."""
def __init__(self) -> None:
self._parts: List[str] = []
def get_html(self) -> str:
return "\n".join(self._parts)
def visit_paragraph(self, paragraph: Paragraph) -> None:
self._parts.append(f"<p>{paragraph.text}</p>")
def visit_heading(self, heading: Heading) -> None:
self._parts.append(f"<h{heading.level}>{heading.text}</h{heading.level}>")
def visit_image(self, image: Image) -> None:
self._parts.append(f'<img src="{image.src}" alt="{image.alt_text}">')
def visit_table(self, table: Table) -> None:
rows_html = []
for i, row in enumerate(table.rows):
tag = "th" if i == 0 else "td"
cells = "".join(f"<{tag}>{cell}</{tag}>" for cell in row)
rows_html.append(f" <tr>{cells}</tr>")
self._parts.append("<table>\n" + "\n".join(rows_html) + "\n</table>")
class PlainTextExportVisitor(DocumentVisitor):
"""Renders all document elements as plain text — another visitor, zero changes to elements."""
def __init__(self) -> None:
self._lines: List[str] = []
def get_text(self) -> str:
return "\n".join(self._lines)
def visit_paragraph(self, paragraph: Paragraph) -> None:
self._lines.append(paragraph.text)
def visit_heading(self, heading: Heading) -> None:
underline = ("=" if heading.level == 1 else "-") * len(heading.text)
self._lines.append(f"\n{heading.text}\n{underline}")
def visit_image(self, image: Image) -> None:
self._lines.append(f"[Image: {image.alt_text or image.src}]")
def visit_table(self, table: Table) -> None:
for row in table.rows:
self._lines.append(" | ".join(row))
# ── Client ────────────────────────────────────────────────────────
if __name__ == "__main__":
# Build a document (the element hierarchy)
document: List[DocumentElement] = [
Heading("Annual Report 2025", level=1),
Paragraph("This report summarizes our annual performance and strategic initiatives."),
Heading("Financial Highlights", level=2),
Paragraph("Revenue grew by twenty percent year over year reaching record highs."),
Image("chart.png", alt_text="Revenue growth chart showing upward trend"),
Table([
["Metric", "2024", "2025"],
["Revenue", "$10M", "$12M"],
["Profit", "$2M", "$3M"],
]),
]
print("=== Visitor Pattern: Document Processing ===\n")
# Apply word count visitor — zero changes to document elements
print("--- Word Count ---")
wc = WordCountVisitor()
for element in document:
element.accept(wc)
print(f" Total: {wc.count} words\n")
# Apply HTML export visitor — zero changes to document elements
print("--- HTML Export ---")
html_visitor = HtmlExportVisitor()
for element in document:
element.accept(html_visitor)
print(html_visitor.get_html())
# Apply plain text export visitor — zero changes to document elements
print("\n--- Plain Text Export ---")
txt_visitor = PlainTextExportVisitor()
for element in document:
element.accept(txt_visitor)
print(txt_visitor.get_text())
Production-Ready Example
Now let's build an Abstract Syntax Tree (AST) evaluator — the textbook production use case for the Visitor Pattern. A simple expression language has Number, BinaryOp, and Variable nodes. Multiple visitors operate on the same AST: an EvaluatorVisitor computes the numerical result, a PrettyPrinterVisitor formats the expression as a string, and a TypeCheckerVisitor validates that the expression is well-formed. The AST nodes never change as we add new visitors.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Dict, Optional
import operator
# ── AST Visitor Interface ─────────────────────────────────────────
class AstVisitor(ABC):
@abstractmethod
def visit_number(self, node: "NumberNode") -> object: ...
@abstractmethod
def visit_variable(self, node: "VariableNode") -> object: ...
@abstractmethod
def visit_binary_op(self, node: "BinaryOpNode") -> object: ...
@abstractmethod
def visit_unary_op(self, node: "UnaryOpNode") -> object: ...
# ── AST Element Interface ─────────────────────────────────────────
class AstNode(ABC):
@abstractmethod
def accept(self, visitor: AstVisitor) -> object: ...
# ── Concrete AST Nodes ────────────────────────────────────────────
class NumberNode(AstNode):
def __init__(self, value: float) -> None:
self.value = value
def accept(self, visitor: AstVisitor) -> object:
return visitor.visit_number(self)
class VariableNode(AstNode):
def __init__(self, name: str) -> None:
self.name = name
def accept(self, visitor: AstVisitor) -> object:
return visitor.visit_variable(self)
class BinaryOpNode(AstNode):
SUPPORTED_OPS = {"+", "-", "*", "/", "**"}
def __init__(self, op: str, left: AstNode, right: AstNode) -> None:
if op not in self.SUPPORTED_OPS:
raise ValueError(f"Unsupported operator: {op}")
self.op = op
self.left = left
self.right = right
def accept(self, visitor: AstVisitor) -> object:
return visitor.visit_binary_op(self)
class UnaryOpNode(AstNode):
def __init__(self, op: str, operand: AstNode) -> None:
self.op = op # "-" for negation
self.operand = operand
def accept(self, visitor: AstVisitor) -> object:
return visitor.visit_unary_op(self)
# ── Concrete Visitors ─────────────────────────────────────────────
class EvaluatorVisitor(AstVisitor):
"""Evaluates the AST to a numerical result given a variable environment."""
OPERATORS = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv,
"**": operator.pow,
}
def __init__(self, env: Optional[Dict[str, float]] = None) -> None:
self._env = env or {}
def visit_number(self, node: NumberNode) -> float:
return node.value
def visit_variable(self, node: VariableNode) -> float:
if node.name not in self._env:
raise NameError(f"Undefined variable: '{node.name}'")
return self._env[node.name]
def visit_binary_op(self, node: BinaryOpNode) -> float:
left = node.left.accept(self)
right = node.right.accept(self)
if node.op == "/" and right == 0:
raise ZeroDivisionError("Division by zero in expression")
return self.OPERATORS[node.op](left, right)
def visit_unary_op(self, node: UnaryOpNode) -> float:
val = node.operand.accept(self)
return -val if node.op == "-" else val
class PrettyPrinterVisitor(AstVisitor):
"""Formats the AST as a human-readable infix expression string."""
def visit_number(self, node: NumberNode) -> str:
return str(int(node.value) if node.value == int(node.value) else node.value)
def visit_variable(self, node: VariableNode) -> str:
return node.name
def visit_binary_op(self, node: BinaryOpNode) -> str:
left = node.left.accept(self)
right = node.right.accept(self)
# Parenthesize if operand is a BinaryOp (simplistic precedence handling)
if isinstance(node.left, BinaryOpNode): left = f"({left})"
if isinstance(node.right, BinaryOpNode): right = f"({right})"
return f"{left} {node.op} {right}"
def visit_unary_op(self, node: UnaryOpNode) -> str:
operand = node.operand.accept(self)
if isinstance(node.operand, BinaryOpNode):
operand = f"({operand})"
return f"{node.op}{operand}"
class TypeCheckerVisitor(AstVisitor):
"""Validates the AST: checks all variables are declared and no division by literal zero."""
def __init__(self, declared_vars: set[str]) -> None:
self._declared = declared_vars
self.errors: list[str] = []
def visit_number(self, node: NumberNode) -> None:
pass # Numbers are always valid
def visit_variable(self, node: VariableNode) -> None:
if node.name not in self._declared:
self.errors.append(f"Undeclared variable: '{node.name}'")
def visit_binary_op(self, node: BinaryOpNode) -> None:
node.left.accept(self)
node.right.accept(self)
if node.op == "/" and isinstance(node.right, NumberNode) and node.right.value == 0:
self.errors.append("Division by literal zero detected")
def visit_unary_op(self, node: UnaryOpNode) -> None:
node.operand.accept(self)
@property
def is_valid(self) -> bool:
return len(self.errors) == 0
class LatexExportVisitor(AstVisitor):
"""Renders the AST as a LaTeX math expression."""
def visit_number(self, node: NumberNode) -> str:
return str(int(node.value) if node.value == int(node.value) else node.value)
def visit_variable(self, node: VariableNode) -> str:
return f"\\{node.name}" if len(node.name) > 1 else node.name
def visit_binary_op(self, node: BinaryOpNode) -> str:
left = node.left.accept(self)
right = node.right.accept(self)
if node.op == "/":
return f"\\frac{{{left}}}{{{right}}}"
if node.op == "**":
return f"{left}^{{{right}}}"
if isinstance(node.left, BinaryOpNode): left = f"\\left({left}\\right)"
if isinstance(node.right, BinaryOpNode): right = f"\\left({right}\\right)"
return f"{left} {node.op} {right}"
def visit_unary_op(self, node: UnaryOpNode) -> str:
operand = node.operand.accept(self)
if isinstance(node.operand, BinaryOpNode):
operand = f"\\left({operand}\\right)"
return f"{node.op}{operand}"
# ── Client ────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== AST Visitor Pattern ===\n")
# Build AST for: (x + 2) * (y - 1)
ast1 = BinaryOpNode("*",
BinaryOpNode("+", VariableNode("x"), NumberNode(2)),
BinaryOpNode("-", VariableNode("y"), NumberNode(1)),
)
printer = PrettyPrinterVisitor()
checker = TypeCheckerVisitor(declared_vars={"x", "y"})
evaluator = EvaluatorVisitor(env={"x": 3.0, "y": 5.0})
latex = LatexExportVisitor()
print(f"Expression : {ast1.accept(printer)}")
ast1.accept(checker)
print(f"Type check : {'✅ Valid' if checker.is_valid else '❌ ' + str(checker.errors)}")
print(f"Evaluate : {ast1.accept(evaluator)} (x=3, y=5)")
print(f"LaTeX : {ast1.accept(latex)}")
# Build AST for: x ** 2 + -y (with an undeclared variable z mixed in for type check demo)
print("\n--- Expression with undeclared variable ---")
ast2 = BinaryOpNode("+",
BinaryOpNode("**", VariableNode("x"), NumberNode(2)),
UnaryOpNode("-", VariableNode("z")), # z is not declared!
)
checker2 = TypeCheckerVisitor(declared_vars={"x", "y"})
ast2.accept(checker2)
print(f"Expression : {ast2.accept(printer)}")
print(f"Type check : {'✅ Valid' if checker2.is_valid else '❌ Errors: ' + str(checker2.errors)}")
print(f"LaTeX : {ast2.accept(latex)}")
# Demonstrate adding a visitor with zero element changes
print("\n--- Division by zero detection ---")
ast3 = BinaryOpNode("/", VariableNode("x"), NumberNode(0))
checker3 = TypeCheckerVisitor(declared_vars={"x"})
ast3.accept(checker3)
print(f"Expression : {ast3.accept(printer)}")
print(f"Type check : {'✅ Valid' if checker3.is_valid else '❌ Errors: ' + str(checker3.errors)}")
Real-World Use Cases
1. Compiler and Interpreter ASTs
Every compiler and interpreter uses the Visitor Pattern on its Abstract Syntax Tree. The same AST is visited by a type checker, a constant folder, an optimizer, a code generator, and a source formatter. Each pass is a separate visitor. The AST node classes (IfStatement, FunctionCall, BinaryExpression, Literal) never change as new compiler passes are added. LLVM, GCC, Roslyn (C#), and the Java compiler all use this architecture.
2. Document Processing and Export
Office suites, static site generators, and content management systems use the Visitor Pattern to process document models. The same DOM (Paragraph, Heading, Image, Table, List) can be visited by an HTML exporter, a PDF renderer, a Markdown exporter, a word counter, a spell checker, and a search indexer — all independent visitor classes operating on the same stable document model.
3. File System Operations
File system traversal tools apply multiple operations to the same tree of File and Directory nodes: calculating total size, searching for files matching a pattern, deleting temporary files, changing permissions, generating a manifest. Each operation is a visitor. The File and Directory classes never change; the operations arrive as new visitors.
4. UI Rendering Pipelines
GUI frameworks with component trees use the Visitor Pattern for rendering passes. A Button, Label, Panel, and ScrollView hierarchy can be visited by a layout calculator, a paint visitor, a hit-test visitor (which component was clicked), an accessibility tree exporter, and a serialization visitor for UI snapshots. Each rendering concern is an independent visitor.
5. Tax and Financial Calculation Engines
Financial systems with different instrument types (Stock, Bond, Option, ETF, Futures) calculate taxes differently, report differently, and hedge differently. Each financial operation — tax calculation, mark-to-market valuation, risk exposure, regulatory reporting — is a visitor that knows how to handle each instrument type. Adding a new reporting requirement means adding a new visitor, not modifying any instrument class.
6. XML/JSON Schema Validation
Schema validators for structured data formats visit the node hierarchy (ObjectNode, ArrayNode, StringNode, NumberNode, BooleanNode) with a validation visitor that checks each node against its schema rules. A separate TransformVisitor converts the node tree from one schema to another. A PathExtractorVisitor finds all nodes matching a JSONPath expression. The node hierarchy is stable; the operations are visitors.
7. Game World and Scene Graph Processing
3D game engines and scene graph managers use the Visitor Pattern on scene nodes (MeshNode, LightNode, CameraNode, TransformNode, ParticleSystemNode). A RenderVisitor builds the render queue, a PhysicsVisitor collects collidable objects, a CullingVisitor prunes nodes outside the camera frustum, a SerializationVisitor saves the scene to disk, and an AnimationVisitor updates transforms each frame. The scene graph never changes; passes are visitors.
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Using isinstance Checks Instead of the Visitor Pattern
# BAD: Type-checking sprawl — every new operation requires editing this function
def export_to_html(element) -> str:
if isinstance(element, Paragraph):
return f"<p>{element.text}</p>"
elif isinstance(element, Heading):
return f"<h{element.level}>{element.text}</h{element.level}>"
elif isinstance(element, Image):
return f'<img src="{element.src}">'
elif isinstance(element, Table):
return "<table>...</table>"
else:
raise TypeError(f"Unknown element: {type(element)}")
# Adding a new operation (Markdown export) means writing another function with the same pattern.
# Adding a new element (Video) means finding and editing EVERY such function.
# GOOD: Visitor Pattern — new operations are new classes; element dispatch is automatic
class HtmlExportVisitor(DocumentVisitor):
def visit_paragraph(self, p): return f"<p>{p.text}</p>"
def visit_heading(self, h): return f"<h{h.level}>{h.text}</h{h.level}>"
def visit_image(self, i): return f'<img src="{i.src}">'
def visit_table(self, t): return "<table>...</table>"
❌ Mistake #2: Forgetting to Return Values from visit*() Methods
# BAD: visit*() methods have side effects but no return value — only works for printing
class HtmlExportVisitor(DocumentVisitor):
def visit_paragraph(self, p: Paragraph) -> None:
print(f"<p>{p.text}</p>") # Side effect only — can't compose or test output
def visit_heading(self, h: Heading) -> None:
print(f"<h{h.level}>{h.text}</h{h.level}>")
# Can't do: html_string = element.accept(visitor)
# Can't test without capturing stdout
# Can't pipe output to another operation
# GOOD: accept() returns a value — visitor is composable and testable
class HtmlExportVisitor(DocumentVisitor):
def __init__(self) -> None:
self._parts: list[str] = []
def get_html(self) -> str:
return "\n".join(self._parts)
def visit_paragraph(self, p: Paragraph) -> None:
self._parts.append(f"<p>{p.text}</p>") # Accumulate for later retrieval
❌ Mistake #3: Putting Traversal Logic Inside the Visitor Instead of the Element
# BAD: Visitor accesses element internals to drive traversal — tight coupling
class WordCountVisitor(DocumentVisitor):
def visit_document(self, doc: Document) -> None:
for element in doc.elements: # Visitor drives traversal — knows Document internals
if isinstance(element, Section):
for sub_el in element.sub_elements: # Reaches into nested structure
sub_el.accept(self)
else:
element.accept(self)
# GOOD: Elements drive their own traversal — visitor just visits
class Section(DocumentElement):
def __init__(self, elements: list) -> None:
self.elements = elements
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_section(self)
for element in self.elements: # Element drives traversal of its children
element.accept(visitor)
Frequently Asked Questions
Q1: What problem does the Visitor Pattern actually solve?
The Visitor Pattern solves the expression problem: given a set of types and a set of operations, you want to be able to add new types without changing existing operations, AND add new operations without changing existing types. Neither classes alone nor function tables alone handle both simultaneously.
Traditional object-oriented design handles adding new types well (just add a subclass) but adding new operations badly (you must add a method to every existing class). The Visitor Pattern flips this: it handles adding new operations well (just add a new visitor class) but adding new types badly (you must add a visit*() method to every existing visitor).
The Visitor Pattern is the right choice when your hierarchy is stable (types rarely change) but your operations are growing (you keep adding new things to do with those types).
Q2: What is double dispatch, and why does the Visitor Pattern need it?
Single dispatch is what most OOP languages support: element.render() selects the right render() method based on the runtime type of element. The receiver's type selects the method.
Double dispatch selects the right method based on the runtime types of two objects — in this case, the element AND the visitor. Without double dispatch support, calling visitor.visit(element) would only dispatch on the visitor's type — it couldn't distinguish between a Paragraph and a Heading argument unless the visitor used instanceof checks.
The Visitor Pattern manually implements double dispatch via the accept/visit handshake:
element.accept(visitor)— dispatches on element's type → callsvisitor.visitParagraph(this)(if element is a Paragraph)visitor.visitParagraph(paragraph)— dispatches on visitor's type → calls the right visitor's method
The result: the exact method called depends on both the runtime type of the element (via the accept() override) and the runtime type of the visitor (via normal polymorphic dispatch). No instanceof, no casting.
Q3: When should I use Visitor instead of adding a method to each class?
Use adding a method when:
- The operation is fundamental to the class's identity (e.g.,
area()on aShape) - The operation will be needed by almost every client of the class
- You expect to add new classes more often than new operations
- The hierarchy is shallow and the operation is simple
Use Visitor when:
- The operation is external to the class's core concern (e.g.,
spellCheck()on aParagraph— spell checking is not a document structure concern) - You expect to add many new operations over time without changing the class hierarchy
- The operation needs to accumulate state across multiple elements
- Multiple unrelated operations need to be applied to the same hierarchy
- The class hierarchy is stable but the set of operations is not
A good heuristic: if the method belongs to the class's "vocabulary" (what the class is and does), add it to the class. If the method is a use of the class from an external perspective (what you do with it), make it a visitor.
Q4: What are the downsides of the Visitor Pattern?
The Visitor Pattern has significant trade-offs:
Adding new element types is hard: Every time you add a new element class (e.g., VideoNode to a document model), you must add a visitVideo() method to every existing visitor interface and every concrete visitor class. If you have 20 visitors, you edit 21 files. This is the flip side of making operations easy to add.
Breaks encapsulation: Visitors often need access to the element's internals to do their work. This can force elements to expose fields or methods as public that would otherwise be protected or package-private. The visitor and the element become closely coupled.
Circular dependencies: The visitor interface must reference all element types; each element type must reference the visitor interface. This creates a circular reference chain that can complicate module organization in languages with strict dependency rules.
Complexity overhead: For small hierarchies with few operations, the pattern is overkill — a simple if/else or switch handles it more directly with less boilerplate.
Q5: How do Rust enums with match relate to the Visitor Pattern?
In Rust, an enum with match provides a form of exhaustive pattern matching that handles the same dispatch problem the Visitor Pattern solves in other languages — and it's enforced by the compiler.
enum DocumentElement { Para(String), Heading(String, usize) }
fn to_html(el: &DocumentElement) -> String {
match el {
DocumentElement::Para(text) => format!("<p>{}</p>", text),
DocumentElement::Heading(text, n) => format!("<h{0}>{1}</h{0}>", n, text),
// Missing any variant = compile error — exhaustiveness enforced!
}
}
This is essentially the Visitor Pattern without the accept/visit indirection — it's the compiler-native equivalent. The trade-off is the same as the Visitor Pattern: adding a new variant means updating every match expression, just as adding a new element type means updating every visitor.
For Rust, match on an enum is idiomatic for closed type sets. Box<dyn Trait> with the full Visitor Pattern (trait + accept()) is used for open type sets that need runtime extensibility.
Q6: Can a visitor return a value?
Yes — and this is often cleaner than accumulating state. Use a generic return type on the visitor interface:
interface AstVisitor<T> {
visitNumber(n: NumberNode): T;
visitVariable(v: VariableNode): T;
visitBinaryOp(n: BinaryOpNode): T;
}
class EvaluatorVisitor implements AstVisitor<number> { /* returns numbers */ }
class PrinterVisitor implements AstVisitor<string> { /* returns strings */ }
class CheckerVisitor implements AstVisitor<boolean> { /* returns booleans */ }
With return values, accept() also returns the visitor's result:
interface AstNode { <T> T accept(AstVisitor<T> v); }
// ...
double result = ast.accept(new EvaluatorVisitor(env));
String pretty = ast.accept(new PrettyPrinterVisitor());
This style — common in Java, TypeScript, Kotlin, and Rust — makes visitors pure functions (given the same AST, produce the same output) which are easier to test and compose.
Q7: How does the Visitor Pattern work with composite structures (trees)?
The Visitor Pattern works naturally with the Composite Pattern (trees of objects). Each composite node's accept() method calls the visitor on itself AND recursively calls accept() on all its children:
class Section(DocumentElement):
def __init__(self, elements: list) -> None:
self.elements = elements
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_section(self) # Visit the section itself
for element in self.elements:
element.accept(visitor) # Recursively visit all children
The visitor doesn't need to know anything about the tree structure — it just handles each node type as it arrives. The tree handles its own traversal.
For fine-grained traversal control, the visitor can decide whether to continue into children:
def visit_section(self, section: Section) -> None:
if self._should_skip_section(section):
return # Don't recurse into this section's children
# Otherwise, section.accept() continues to call accept() on children
Q8: How do I handle the Visitor Pattern when I can't modify the element classes?
When you cannot add accept(visitor) to existing classes (third-party classes, legacy code, value types), you have two options:
External visitor via isinstance/instanceof: Use type-checked dispatch in the visitor instead of double dispatch. This loses the compile-time exhaustiveness guarantee but works without modifying the element classes:
def visit(self, element: object) -> None:
if isinstance(element, Paragraph):
self.visit_paragraph(element)
elif isinstance(element, Heading):
self.visit_heading(element)
else:
raise TypeError(f"Unknown element type: {type(element)}")
Adapter/Wrapper: Wrap the unmodifiable classes in your own element classes that implement accept(). The wrappers delegate all other operations to the wrapped object.
Q9: Should I use Visitor or just a switch/match statement?
For small, closed, stable hierarchies (3-5 types, known at compile time, unlikely to change), a switch/match/when expression is often the simpler and more idiomatic choice. Languages like Kotlin, Rust, Swift, and C# with sealed classes and exhaustive pattern matching enforce completeness at the compiler level — a missing case is a compile error.
For larger, open, growing hierarchies where you expect many operations to be added over time, the Visitor Pattern pays off. The visitor interface centralizes the "contract" of all element types, and each operation class is self-contained and independently testable.
The practical threshold: if you have more than ~5 operations on more than ~5 element types, consider the Visitor Pattern. Below that threshold, switch expressions are likely cleaner.
Q10: How do I test code using the Visitor Pattern?
The Visitor Pattern dramatically improves testability in both directions:
Testing visitors independently: Create an instance of each element type directly and call accept() with a specific visitor. Each visitor is a pure unit — given the same elements, it should produce the same output. No need to mock or stub; the elements are just data objects.
def test_html_export_paragraph():
visitor = HtmlExportVisitor()
Paragraph("Hello world").accept(visitor)
assert visitor.get_html() == "<p>Hello world</p>"
def test_word_count_table():
visitor = WordCountVisitor()
Table([["Metric", "Value"], ["Revenue", "Ten million"]]).accept(visitor)
assert visitor.count == 4 # 4 words total in the table
Testing elements independently: An element's only job is to call the right visit*() method with itself. Test this with a mock visitor that records which method was called:
def test_paragraph_calls_visit_paragraph():
mock = MagicMock(spec=DocumentVisitor)
para = Paragraph("test")
para.accept(mock)
mock.visit_paragraph.assert_called_once_with(para)
Key Takeaways
🎯 Core Concept: The Visitor Pattern separates operations from the objects they operate on. A stable class hierarchy (Paragraph, Heading, Image, Table) keeps only one new method — accept(visitor) — which calls the visitor's corresponding method with itself (visitor.visitParagraph(this)). All operation logic lives in visitor classes, not in the element classes. Adding a new operation means adding a new visitor class — the element hierarchy never changes. This is the pattern that powers compilers, document processors, and every system where a fixed data model needs an ever-growing set of operations.
🔑 Key Benefits:
- Open/Closed for operations: New operations are new visitor classes — no changes to element classes
- Single Responsibility: Each visitor class owns exactly one operation across the entire hierarchy
- Double dispatch without casting: The
accept/visithandshake dispatches on both the element's type and the visitor's type — noinstanceof, no casts - Accumulate state across traversal: Visitors gather results as they traverse — a word counter, error collector, or render buffer all fit naturally
- Cross-cutting cohesion: All logic for one operation lives together in one class rather than scattered across every element class
🌐 Language-Specific Highlights:
- Python: Use
ABCand@abstractmethodfor both the visitor interface and the element interface; avoidisinstance-based dispatch — that's the anti-pattern the Visitor Pattern replaces; use avisit_*naming convention; prefer returning values from visitors for composability - TypeScript: Use a generic return type
AstVisitor<T>on the visitor interface — eliminates mutable accumulation for pure visitors; keep concrete element classes unexported; useinterface(notabstract class) for the visitor contract - Java: Use generics on the visitor interface (
AstVisitor<T>) for type-safe return values; userecordfor immutable element value objects; markaccept()with the visitor's exact concrete type parameter; avoid raw-type visitor interfaces - JavaScript: Never dispatch via
this.constructor.name— minifiers break it; always use explicit method calls inaccept(); use#privatefields for visitor state accumulation; use classes (not plain objects) for visitors with state - C#: Use
sealed recordfor immutable elements; for closed hierarchies, considerswitchexpressions with pattern matching as a simpler alternative to full Visitor; keepIDocumentVisitorparameter types strongly typed; avoiddynamicdispatch - PHP: Use PHP 8.0+ constructor property promotion for element constructors; type all
visit*()parameters; usereadonlyproperties on elements;finalon concrete elements where appropriate - Go: No inheritance means interfaces for both visitors and elements; use pointer receivers (
*VisitorType) for stateful visitors — value receivers silently discard state mutations; embed a base struct with no-op methods to avoid forcing all element types to implement every visitor method; store elements as interface slices - Rust: Use
&mut selfon visitor trait methods when the visitor accumulates state; implementaccept()on enums (not just structs) to centralize dispatch; useBox<dyn Trait>for element collections; usematchon enums for closed type sets — it's the idiomatic Rust equivalent - Dart: Use
abstract classfor both the visitor and element interfaces; Dart 3sealed classfor closed element hierarchies with exhaustiveswitchdispatch; type allvisit*()parameters explicitly; avoiddynamicvisitor parameter types - Swift: Use
protocolfor both visitor and element; mark visitor methodsmutatingwhen using struct-based visitors with state; useinout some DocumentVisitorfor generic visitor passing; useanyexistentials for heterogeneous visitor collections - Kotlin: Use
fun interfacefor single-method visitor variants;objectfor stateless singleton visitors (no allocation per use);data classfor immutable element value objects; for closed sealed hierarchies with few operations,whenexpressions are idiomatic — use full Visitor for growing operation sets
📋 When to Use Visitor:
- You have a stable class hierarchy (types rarely change) but a growing set of operations (new things to do with those types)
- You need to add new operations without touching the existing class hierarchy
- An operation traverses multiple unrelated classes and accumulates state
- You want each operation's logic in one place rather than scattered across all element classes
- The elements need to be processed by operations that are defined externally — compiler passes, exporters, validators, analyzers
⚠️ When NOT to Use Visitor:
- The class hierarchy changes frequently — every new element type breaks all existing visitors
- The hierarchy is small (3-5 types) and operations are few — a
switch/match/whenexpression is cleaner - Encapsulation is critical — visitors often need access to element internals, increasing coupling
- You're in a language with native multiple dispatch (Julia, CLOS) — the pattern is unnecessary
- Operations are simple enough to add directly as methods — don't over-engineer
🛠️ Best Practices Across Languages:
- Implement
accept()in every concrete element — never try to dispatch from a base class using type checks; each concrete class knows its own type and calls the rightvisit*()method - Use typed/generic return values — visitor methods that return
Trather thanvoidmake visitors pure functions that are easier to test and compose - Keep visitor state private — the visitor's accumulated result (count, HTML string, error list) should only be accessible via a getter, not mutated from outside
- Let elements drive traversal of their children —
accept()should call the visitor onthis, then callaccept()on child elements; visitors should not need to know the element's internal structure - One visitor interface per concern — don't create a god visitor interface with methods for unrelated element hierarchies; separate interfaces for separate hierarchies
- Use
objector module-level singletons for stateless visitors (Kotlin/Python/Go) — stateless visitors don't need to be instantiated per use - Make elements immutable — elements are data; use records, data classes, or frozen objects to prevent visitors from accidentally modifying elements during traversal
💡 Common Pitfalls:
instanceof-based dispatch in the visitor: If a visitor checksif element instanceof Paragraphinstead of usingaccept(), the double dispatch is broken — you've re-introduced the type-checking that the pattern was designed to eliminate- Dispatching from the base class via type checks: A base
DocumentElementclass that callsvisitor.visitParagraph(this)only ifthis instanceof Paragraphis wrong — each concrete element must overrideaccept()itself - Value receiver instead of pointer receiver in Go: A value receiver visitor silently discards all state mutations — the count is always zero, the HTML is always empty
- Mixing traversal responsibility: Visitors should not traverse into elements' internals; elements'
accept()methods should drive traversal of their own children - Stateful visitor reuse without reset: A visitor that accumulates state (word count, HTML parts) must be freshly instantiated (or reset) for each document — reusing the same instance across multiple documents produces incorrect cumulative results
- Not handling all element types: A visitor that silently does nothing for some element types (instead of throwing or logging) hides bugs — missing
visit*()implementations are always errors for non-trivial operations
🎁 Advanced Techniques:
- Generic/typed return values:
interface AstVisitor<T>withT visitNumber(NumberNode n)lets evaluators returnDouble, printers returnString, and checkers returnBoolean— all from the same visitor structure, no mutable accumulation needed - Visitor + Composite: Combine Visitor with the Composite Pattern so composite nodes recursively call
accept()on their children — the visitor traverses the entire tree automatically, visiting every node exactly once - Visitor registry: A
Map<Class<?>, Consumer<Object>>maps element types to visit functions — enables dynamic visitor registration without modifying the visitor interface, at the cost of type safety - Intercepting visitor: Wrap a visitor to add cross-cutting behavior (logging, timing, filtering) around every
visit*()call — aLoggingVisitorwraps anyDocumentVisitorand logs before and after each visit - Multi-pass visitors: Chain multiple visitors on the same traversal —
PassOneVisitorcollects symbol tables;PassTwoVisitoruses them for type checking. In compilers, multiple passes over the same AST are standard. - Visitor + Observer: Notify observers when specific element types are visited — a
ProgressObserverreceives events as a large document tree is processed, enabling progress bars and streaming output - Lazy/streaming visitors: For very large element hierarchies, a visitor can be a coroutine (Python generator, Kotlin sequence, Rust async stream) that yields results one at a time instead of accumulating everything in memory first
The Visitor Pattern is the right tool when you have a fixed data model and a growing need to do different things with it. It appears in every serious compiler, document processor, and analysis tool. Once you understand it, you'll see it in LLVM's instruction visitors, Java's bytecode manipulation libraries, Spring's bean definition visitors, and CSS selector engines. It is the canonical solution to the expression problem — the cleanest way to add operations to a type hierarchy without touching the types themselves.
This guide completes the behavioral design patterns series. You now have comprehensive coverage of:
- Memento Pattern — Capture and restore object state
- Observer Pattern — Event-driven notifications
- State Pattern — Behavior changes with lifecycle phase
- Strategy Pattern — Interchangeable algorithms
- Template Method Pattern — Algorithm skeleton with variable steps
- Visitor Pattern — New operations on a stable hierarchy
Each guide covers all 11 programming languages with production-ready examples, language-specific best practices, and comprehensive anti-pattern analysis.