Visitor Pattern: Add Operations Without Changing Classes

Updated 2026-03-11

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.

Prerequisites: Understanding of polymorphism and interfaces, familiarity with the Strategy Pattern, knowledge of method overloading, and basic understanding of the Single Responsibility Principle. You should be comfortable with abstract classes and inheritance hierarchies.

After this topic: Implement the Visitor Pattern to add new operations to object structures without modifying existing classes, identify when the pattern is appropriate versus when it adds unnecessary complexity, and explain the double dispatch mechanism that makes the pattern work.

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:

  1. You call element.accept(visitor)
  2. The element calls back visitor.visit(self) with its specific type
  3. 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"![{image.alt_text}]({image.url})"
    
    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

![Company Logo](logo.png)
| 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:

  1. Identify stable structure (File, Directory) vs. changing operations (size, count, search)
  2. Create Element interface with accept(visitor)
  3. Create Visitor interface with visit_file() and visit_directory()
  4. Implement concrete visitors for each operation
  5. 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 by visitor.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.