Visitor Pattern: Add Operations Without Changing Classes
TL;DR
The Visitor Pattern lets you add new operations to existing object structures without modifying those structures. It separates algorithms from the objects they operate on by moving the operation logic into separate visitor classes. This pattern is essential when you need to perform many different operations on a stable object structure.
Core Concept
What is the Visitor Pattern?
The Visitor Pattern is a behavioral design pattern that separates an algorithm from the object structure it operates on. Instead of adding methods to each class in a hierarchy, you create separate visitor classes that contain the operations. The objects “accept” visitors and let them perform operations.
The Core Problem
Imagine you have a stable class hierarchy (like different types of shapes or document elements) and you need to add new operations frequently (calculate area, export to XML, render to screen). Without the Visitor Pattern, you’d add a new method to every class each time. This violates the Open/Closed Principle — your classes should be open for extension but closed for modification.
How It Works: Double Dispatch
The Visitor Pattern uses double dispatch — a technique where the method called depends on both the visitor type AND the element type. Here’s the flow:
- You call
element.accept(visitor) - The element calls back
visitor.visit(self)with its specific type - The visitor executes the appropriate method for that element type
This two-step process ensures the right operation runs for the right element type.
Key Components
Visitor Interface: Declares visit methods for each concrete element type.
Concrete Visitors: Implement specific operations for each element type.
Element Interface: Declares an accept(visitor) method.
Concrete Elements: Implement accept() by calling visitor.visit(self).
Object Structure: A collection of elements that can be visited.
When to Use
Use the Visitor Pattern when:
- You have a stable object structure but need to add many new operations
- Operations are unrelated to each other (rendering, exporting, validating)
- You want to keep related operations together in one class
- You’re willing to accept the complexity of the pattern
Avoid it when your object structure changes frequently — adding new element types requires updating all visitors.
Visual Guide
Visitor Pattern Structure
classDiagram
class Visitor {
<<interface>>
+visit_concrete_element_a(element)*
+visit_concrete_element_b(element)*
}
class ConcreteVisitor1 {
+visit_concrete_element_a(element)
+visit_concrete_element_b(element)
}
class ConcreteVisitor2 {
+visit_concrete_element_a(element)
+visit_concrete_element_b(element)
}
class Element {
<<interface>>
+accept(visitor)*
}
class ConcreteElementA {
+accept(visitor)
+operation_a()
}
class ConcreteElementB {
+accept(visitor)
+operation_b()
}
Visitor <|.. ConcreteVisitor1
Visitor <|.. ConcreteVisitor2
Element <|.. ConcreteElementA
Element <|.. ConcreteElementB
ConcreteVisitor1 ..> ConcreteElementA : visits
ConcreteVisitor1 ..> ConcreteElementB : visits
ConcreteVisitor2 ..> ConcreteElementA : visits
ConcreteVisitor2 ..> ConcreteElementB : visits
The Visitor Pattern separates operations (visitors) from the object structure (elements). Each visitor implements operations for all element types.
Double Dispatch Flow
sequenceDiagram
participant Client
participant Element
participant Visitor
Client->>Element: accept(visitor)
Note over Element: Element knows its own type
Element->>Visitor: visit_concrete_element(self)
Note over Visitor: Visitor knows operation to perform
Visitor->>Visitor: Execute operation for this element type
Visitor-->>Element: Return result
Element-->>Client: Return result
Double dispatch: the element calls back to the visitor with its specific type, ensuring the correct operation executes.
Examples
Example 1: Document Export System
Let’s build a document structure with different element types (paragraphs, images, tables) and add export operations without modifying the element classes.
from abc import ABC, abstractmethod
from typing import List
# Element interface
class DocumentElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# Concrete elements
class Paragraph(DocumentElement):
def __init__(self, text: str):
self.text = text
def accept(self, visitor):
return visitor.visit_paragraph(self)
class Image(DocumentElement):
def __init__(self, url: str, alt_text: str):
self.url = url
self.alt_text = alt_text
def accept(self, visitor):
return visitor.visit_image(self)
class Table(DocumentElement):
def __init__(self, rows: List[List[str]]):
self.rows = rows
def accept(self, visitor):
return visitor.visit_table(self)
# Visitor interface
class DocumentVisitor(ABC):
@abstractmethod
def visit_paragraph(self, paragraph: Paragraph):
pass
@abstractmethod
def visit_image(self, image: Image):
pass
@abstractmethod
def visit_table(self, table: Table):
pass
# Concrete visitor: HTML export
class HTMLExporter(DocumentVisitor):
def visit_paragraph(self, paragraph: Paragraph) -> str:
return f"<p>{paragraph.text}</p>"
def visit_image(self, image: Image) -> str:
return f'<img src="{image.url}" alt="{image.alt_text}" />'
def visit_table(self, table: Table) -> str:
html = "<table>\n"
for row in table.rows:
html += " <tr>\n"
for cell in row:
html += f" <td>{cell}</td>\n"
html += " </tr>\n"
html += "</table>"
return html
# Concrete visitor: Markdown export
class MarkdownExporter(DocumentVisitor):
def visit_paragraph(self, paragraph: Paragraph) -> str:
return f"{paragraph.text}\n"
def visit_image(self, image: Image) -> str:
return f""
def visit_table(self, table: Table) -> str:
if not table.rows:
return ""
# Header row
md = "| " + " | ".join(table.rows[0]) + " |\n"
md += "| " + " | ".join(["---"] * len(table.rows[0])) + " |\n"
# Data rows
for row in table.rows[1:]:
md += "| " + " | ".join(row) + " |\n"
return md
# Usage
document = [
Paragraph("Welcome to our guide"),
Image("logo.png", "Company Logo"),
Table([["Name", "Age"], ["Alice", "30"], ["Bob", "25"]])
]
# Export to HTML
html_exporter = HTMLExporter()
print("HTML Export:")
for element in document:
print(element.accept(html_exporter))
# Export to Markdown
md_exporter = MarkdownExporter()
print("\nMarkdown Export:")
for element in document:
print(element.accept(md_exporter))
Expected Output:
HTML Export:
<p>Welcome to our guide</p>
<img src="logo.png" alt="Company Logo" />
<table>
<tr>
<td>Name</td>
<td>Age</td>
</tr>
<tr>
<td>Alice</td>
<td>30</td>
</tr>
<tr>
<td>Bob</td>
<td>25</td>
</tr>
</table>
Markdown Export:
Welcome to our guide

| Name | Age |
| --- | --- |
| Alice | 30 |
| Bob | 25 |
Key Points:
- Adding a new export format (JSON, PDF) requires only a new visitor class
- Element classes remain unchanged
- Each visitor keeps all related export logic together
Try it yourself: Add a PlainTextExporter visitor that strips all formatting and returns plain text.
Example 2: Shopping Cart with Tax Calculation
Different product types have different tax rules. The Visitor Pattern lets us add new tax strategies without modifying product classes.
from abc import ABC, abstractmethod
from enum import Enum
class ProductCategory(Enum):
FOOD = "food"
ELECTRONICS = "electronics"
CLOTHING = "clothing"
# Element interface
class Product(ABC):
def __init__(self, name: str, price: float):
self.name = name
self.price = price
@abstractmethod
def accept(self, visitor):
pass
# Concrete elements
class FoodProduct(Product):
def __init__(self, name: str, price: float, is_organic: bool):
super().__init__(name, price)
self.is_organic = is_organic
def accept(self, visitor):
return visitor.visit_food(self)
class ElectronicsProduct(Product):
def __init__(self, name: str, price: float, warranty_years: int):
super().__init__(name, price)
self.warranty_years = warranty_years
def accept(self, visitor):
return visitor.visit_electronics(self)
class ClothingProduct(Product):
def __init__(self, name: str, price: float, size: str):
super().__init__(name, price)
self.size = size
def accept(self, visitor):
return visitor.visit_clothing(self)
# Visitor interface
class ProductVisitor(ABC):
@abstractmethod
def visit_food(self, product: FoodProduct):
pass
@abstractmethod
def visit_electronics(self, product: ElectronicsProduct):
pass
@abstractmethod
def visit_clothing(self, product: ClothingProduct):
pass
# Concrete visitor: US tax calculator
class USTaxCalculator(ProductVisitor):
def visit_food(self, product: FoodProduct) -> float:
# Food is tax-exempt in many US states
return 0.0
def visit_electronics(self, product: ElectronicsProduct) -> float:
# 8.5% sales tax on electronics
return product.price * 0.085
def visit_clothing(self, product: ClothingProduct) -> float:
# 6% sales tax on clothing
return product.price * 0.06
# Concrete visitor: EU VAT calculator
class EUVATCalculator(ProductVisitor):
def visit_food(self, product: FoodProduct) -> float:
# Reduced VAT rate for food: 5%
return product.price * 0.05
def visit_electronics(self, product: ElectronicsProduct) -> float:
# Standard VAT rate: 20%
return product.price * 0.20
def visit_clothing(self, product: ClothingProduct) -> float:
# Standard VAT rate: 20%
return product.price * 0.20
# Concrete visitor: Shipping cost calculator
class ShippingCalculator(ProductVisitor):
def visit_food(self, product: FoodProduct) -> float:
# Perishable food needs expedited shipping
return 15.0 if not product.is_organic else 20.0
def visit_electronics(self, product: ElectronicsProduct) -> float:
# Fragile items need special handling
base_cost = 10.0
insurance = product.price * 0.02 # 2% insurance
return base_cost + insurance
def visit_clothing(self, product: ClothingProduct) -> float:
# Standard shipping
return 5.0
# Usage
cart = [
FoodProduct("Organic Apples", 5.99, is_organic=True),
ElectronicsProduct("Laptop", 999.99, warranty_years=2),
ClothingProduct("T-Shirt", 19.99, size="M")
]
us_tax = USTaxCalculator()
eu_vat = EUVATCalculator()
shipping = ShippingCalculator()
print("US Tax Calculation:")
total_us_tax = 0
for product in cart:
tax = product.accept(us_tax)
total_us_tax += tax
print(f"{product.name}: ${tax:.2f}")
print(f"Total US Tax: ${total_us_tax:.2f}")
print("\nEU VAT Calculation:")
total_eu_vat = 0
for product in cart:
vat = product.accept(eu_vat)
total_eu_vat += vat
print(f"{product.name}: ${vat:.2f}")
print(f"Total EU VAT: ${total_eu_vat:.2f}")
print("\nShipping Costs:")
total_shipping = 0
for product in cart:
cost = product.accept(shipping)
total_shipping += cost
print(f"{product.name}: ${cost:.2f}")
print(f"Total Shipping: ${total_shipping:.2f}")
Expected Output:
US Tax Calculation:
Organic Apples: $0.00
Laptop: $85.00
T-Shirt: $1.20
Total US Tax: $86.20
EU VAT Calculation:
Organic Apples: $0.30
Laptop: $200.00
T-Shirt: $4.00
Total EU VAT: $204.30
Shipping Costs:
Organic Apples: $20.00
Laptop: $30.00
T-Shirt: $5.00
Total Shipping: $55.00
Key Points:
- Three different operations (US tax, EU VAT, shipping) without modifying product classes
- Each visitor encapsulates a complete algorithm
- Easy to add new regions or calculation methods
Try it yourself: Add a CanadianTaxCalculator visitor with different tax rates: food 0%, electronics 13%, clothing 13%.
Java/C++ Notes
Java: Use interfaces for Visitor and Element. Method overloading handles different visit methods naturally:
interface Visitor {
void visit(FoodProduct product);
void visit(ElectronicsProduct product);
void visit(ClothingProduct product);
}
C++: Use forward declarations to avoid circular dependencies. Consider using std::variant and std::visit in modern C++17+ as an alternative:
class Visitor {
public:
virtual void visit(FoodProduct& product) = 0;
virtual void visit(ElectronicsProduct& product) = 0;
virtual void visit(ClothingProduct& product) = 0;
virtual ~Visitor() = default;
};
Common Mistakes
1. Adding New Element Types Frequently
Mistake: Using the Visitor Pattern when your object structure changes often.
Why it’s wrong: Every time you add a new element type, you must update ALL visitor classes to handle it. If you add a VideoProduct class, you need to add visit_video() to USTaxCalculator, EUVATCalculator, ShippingCalculator, and every other visitor. This creates a maintenance nightmare.
Fix: Use the Visitor Pattern only when your element hierarchy is stable but you need to add many operations. If your structure changes frequently, consider the Strategy Pattern or simply adding methods to your classes.
2. Breaking Encapsulation
Mistake: Making visitors access private data directly, forcing elements to expose internal state.
# Bad: Visitor needs access to private data
class BadVisitor:
def visit_account(self, account):
# Accessing private balance directly
return account._balance * 0.02 # Violates encapsulation
Why it’s wrong: Elements must expose their internals to visitors, breaking encapsulation. This couples visitors tightly to element implementation details.
Fix: Provide public methods for visitors to use, or pass only necessary data to the visitor:
# Good: Element provides public interface
class Account:
def accept(self, visitor):
return visitor.visit_account(self.get_balance(), self.get_type())
def get_balance(self):
return self._balance
3. Not Using the Visitor Interface
Mistake: Creating visitors without a common interface, making them hard to use interchangeably.
# Bad: No common interface
class HTMLExporter:
def export_paragraph(self, p): pass
class MarkdownExporter:
def convert_paragraph(self, p): pass # Different method name!
Why it’s wrong: Without a common interface, you can’t swap visitors easily or store them in collections. The pattern loses its flexibility.
Fix: Always define a visitor interface with consistent method names:
class DocumentVisitor(ABC):
@abstractmethod
def visit_paragraph(self, paragraph): pass
4. Returning Different Types from Visit Methods
Mistake: Having visit methods return different types, making it hard to aggregate results.
# Bad: Inconsistent return types
class BadVisitor:
def visit_paragraph(self, p) -> str:
return "<p>text</p>"
def visit_image(self, img) -> dict: # Different type!
return {"url": img.url}
Why it’s wrong: You can’t easily collect and combine results from visiting multiple elements.
Fix: Use consistent return types. If you need different data, use a common wrapper type:
class ExportResult:
def __init__(self, content: str, metadata: dict = None):
self.content = content
self.metadata = metadata or {}
class ConsistentVisitor:
def visit_paragraph(self, p) -> ExportResult:
return ExportResult("<p>text</p>")
def visit_image(self, img) -> ExportResult:
return ExportResult(f'<img src="{img.url}" />',
{"type": "image"})
5. Overusing the Pattern for Simple Cases
Mistake: Applying the Visitor Pattern when a simple method would suffice.
# Overkill for a single operation
class Shape(ABC):
@abstractmethod
def accept(self, visitor): pass
class AreaCalculator: # Only one operation!
def visit_circle(self, c): return 3.14 * c.radius ** 2
def visit_square(self, s): return s.side ** 2
Why it’s wrong: The Visitor Pattern adds significant complexity. If you only have one or two operations, just add methods to your classes.
Fix: Use the pattern only when you have multiple unrelated operations:
# Simple case: just add a method
class Shape(ABC):
@abstractmethod
def area(self): pass
class Circle(Shape):
def area(self):
return 3.14 * self.radius ** 2
Interview Tips
What Interviewers Look For
1. Recognize the Trade-off
Interviewers want to see you understand when NOT to use the Visitor Pattern. Say: “The Visitor Pattern is great when you have a stable object structure but need to add many operations. However, it makes adding new element types difficult because every visitor must be updated. I’d use it for something like a compiler AST where node types are fixed but we need many analysis passes.”
2. Explain Double Dispatch Clearly
Be ready to explain: “The Visitor Pattern uses double dispatch. First, we call element.accept(visitor). The element knows its own type, so it calls back with visitor.visit_specific_type(self). This two-step process ensures the right method executes based on both the visitor type AND the element type. In languages without method overloading, this is the only way to achieve type-specific behavior.”
3. Compare with Alternatives
Interviewers may ask: “Why not just use instanceof checks?” Answer: “Using isinstance() or instanceof creates a maintenance problem. Every time you add a new element type, you must find and update all the conditional logic. The Visitor Pattern uses polymorphism instead — the compiler helps you find what needs updating because you must implement all visit methods.”
4. Discuss Real-World Applications
Mention concrete examples:
- Compilers: AST traversal for type checking, optimization, code generation
- Document processing: Export to multiple formats (HTML, PDF, Markdown)
- Graphics: Rendering shapes to different outputs (screen, printer, SVG)
- Tax/pricing systems: Different calculation rules for product categories
5. Code the Pattern Quickly
Practice writing the basic structure from memory:
# Element side
class Element(ABC):
@abstractmethod
def accept(self, visitor): pass
class ConcreteElement(Element):
def accept(self, visitor):
return visitor.visit_concrete_element(self)
# Visitor side
class Visitor(ABC):
@abstractmethod
def visit_concrete_element(self, element): pass
6. Discuss the Open/Closed Principle
Say: “The Visitor Pattern follows the Open/Closed Principle for operations. We can add new operations (visitors) without modifying existing element classes. However, it violates the principle for element types — adding a new element requires modifying all visitors.”
7. Handle the Encapsulation Question
If asked about breaking encapsulation, respond: “Yes, visitors often need access to element internals, which can break encapsulation. We mitigate this by providing public accessor methods rather than exposing fields directly. The trade-off is worth it when we need to separate complex algorithms from the object structure.”
Common Interview Question: “Design a file system where you can calculate total size, count files, and search for patterns without modifying the file/directory classes.”
Your approach:
- Identify stable structure (File, Directory) vs. changing operations (size, count, search)
- Create Element interface with
accept(visitor) - Create Visitor interface with
visit_file()andvisit_directory() - Implement concrete visitors for each operation
- Explain why this is better than adding methods to File/Directory
Red flags to avoid:
- Don’t say “Visitor Pattern is always better than adding methods”
- Don’t ignore the complexity cost
- Don’t forget to mention when the pattern is inappropriate
- Don’t confuse it with the Strategy Pattern (Visitor operates on a structure, Strategy encapsulates a single algorithm)
Key Takeaways
-
The Visitor Pattern separates operations from object structures, letting you add new operations without modifying existing classes. Use it when your object hierarchy is stable but you need many different operations.
-
Double dispatch is the mechanism that makes the pattern work:
element.accept(visitor)followed byvisitor.visit_specific_type(self)ensures the correct operation executes based on both types. -
The pattern has a significant trade-off: adding new operations is easy (just create a new visitor), but adding new element types is hard (must update all existing visitors). Only use it when operations change more frequently than structure.
-
Common use cases include compilers (AST traversal), document export systems, tax/pricing calculations, and graphics rendering — scenarios where you have a fixed structure with many different operations.
-
The pattern can break encapsulation because visitors need access to element internals. Mitigate this by providing public accessor methods and keeping visitor logic focused on algorithms rather than data manipulation.