Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Visitor Pattern

Add new operations without modifying classes - separate algorithms from the objects they operate on!

Visitor Pattern: Operations on Object Structures

Section titled “Visitor Pattern: Operations on Object Structures”

Now let’s dive into the Visitor Pattern - one of the most sophisticated behavioral design patterns that lets you define new operations on objects without changing their classes. It separates algorithms from the object structure they operate on.

Imagine a document editor with different elements - paragraphs, images, tables. You need multiple operations: export to PDF, export to HTML, calculate word count, spell check. Without Visitor, you’d add methods to each element for each operation. With Visitor Pattern, you create separate visitor classes for each operation, keeping elements clean!

The Visitor Pattern lets you add new operations to a class hierarchy without modifying the classes. You define a visitor object that implements all variations of an operation for each class in the hierarchy, and the elements “accept” visitors to let them perform operations.

The Visitor Pattern is useful when:

  1. You need many unrelated operations - On objects in a structure
  2. Object structure rarely changes - But operations change often
  3. You want to avoid polluting classes - Keep operations separate
  4. You need operations across class hierarchy - Different handling per type
  5. You want to accumulate state - Visitor can gather information as it visits

What Happens If We Don’t Use Visitor Pattern?

Section titled “What Happens If We Don’t Use Visitor Pattern?”

Without the Visitor Pattern, you might:


Let’s start with a super simple example that anyone can understand!

Diagram

Here’s how the Visitor Pattern achieves double dispatch:

sequenceDiagram
    participant Client
    participant Circle
    participant AreaCalculator
    
    Client->>Circle: accept(areaCalculator)
    activate Circle
    Note right of Circle: First dispatch:\nbased on Circle type
    
    Circle->>AreaCalculator: visit_circle(this)
    activate AreaCalculator
    Note right of AreaCalculator: Second dispatch:\nbased on visitor type
    
    AreaCalculator->>AreaCalculator: Calculate π × r²
    AreaCalculator-->>Circle: return area
    deactivate AreaCalculator
    
    Circle-->>Client: return result
    deactivate Circle
    
    Note over Client,AreaCalculator: Double dispatch allows different<br/>behavior based on BOTH types!

You’re building a graphics application with different shapes. You need to calculate area, perimeter, and draw operations. Without Visitor Pattern:

bad_shapes.py
# ❌ Without Visitor Pattern - Operations in every class!
import math
class Circle:
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self.radius
def draw(self) -> str:
return f"Drawing circle with radius {self.radius}"
# Adding new operation? Add method here!
def export_svg(self) -> str:
return f'<circle r="{self.radius}"/>'
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
def draw(self) -> str:
return f"Drawing rectangle {self.width}x{self.height}"
# Adding new operation? Add method here too!
def export_svg(self) -> str:
return f'<rect width="{self.width}" height="{self.height}"/>'
class Triangle:
def __init__(self, a: float, b: float, c: float):
self.a, self.b, self.c = a, b, c
def area(self) -> float:
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s-self.a) * (s-self.b) * (s-self.c))
def perimeter(self) -> float:
return self.a + self.b + self.c
def draw(self) -> str:
return f"Drawing triangle with sides {self.a}, {self.b}, {self.c}"
# And here... for EVERY shape!
def export_svg(self) -> str:
return f'<polygon points="..."/>'
# Problems:
# - Every new operation = modify ALL shape classes
# - Related code (all area calcs) spread across files
# - Shapes know about SVG, drawing, etc. (too much responsibility)
# - Violates Open/Closed Principle

Problems:

classDiagram
    class Shape {
        <<interface>>
        +accept(visitor) void
    }
    class Circle {
        -radius: float
        +accept(visitor) void
        +get_radius() float
    }
    class Rectangle {
        -width: float
        -height: float
        +accept(visitor) void
        +get_width() float
        +get_height() float
    }
    class ShapeVisitor {
        <<interface>>
        +visit_circle(circle) void
        +visit_rectangle(rect) void
    }
    class AreaCalculator {
        -total_area: float
        +visit_circle(circle) void
        +visit_rectangle(rect) void
        +get_total() float
    }
    class PerimeterCalculator {
        -total_perimeter: float
        +visit_circle(circle) void
        +visit_rectangle(rect) void
        +get_total() float
    }
    class SVGExporter {
        -svg_elements: List
        +visit_circle(circle) void
        +visit_rectangle(rect) void
        +get_svg() str
    }
    
    Shape <|.. Circle : implements
    Shape <|.. Rectangle : implements
    ShapeVisitor <|.. AreaCalculator : implements
    ShapeVisitor <|.. PerimeterCalculator : implements
    ShapeVisitor <|.. SVGExporter : implements
    Shape --> ShapeVisitor : accepts
    
    note for Shape "Elements accept visitors"
    note for ShapeVisitor "Visitors define operations"
visitor_shapes.py
from abc import ABC, abstractmethod
import math
from typing import List
# Step 1: Define the Visitor interface
class ShapeVisitor(ABC):
"""Visitor interface - defines visit methods for each shape type"""
@abstractmethod
def visit_circle(self, circle: 'Circle') -> None:
pass
@abstractmethod
def visit_rectangle(self, rectangle: 'Rectangle') -> None:
pass
@abstractmethod
def visit_triangle(self, triangle: 'Triangle') -> None:
pass
# Step 2: Define the Element interface
class Shape(ABC):
"""Element interface - shapes accept visitors"""
@abstractmethod
def accept(self, visitor: ShapeVisitor) -> None:
"""Accept a visitor to perform an operation"""
pass
# Step 3: Create Concrete Elements
class Circle(Shape):
"""Concrete element - circle shape"""
def __init__(self, radius: float):
self.radius = radius
def accept(self, visitor: ShapeVisitor) -> None:
visitor.visit_circle(self)
class Rectangle(Shape):
"""Concrete element - rectangle shape"""
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def accept(self, visitor: ShapeVisitor) -> None:
visitor.visit_rectangle(self)
class Triangle(Shape):
"""Concrete element - triangle shape"""
def __init__(self, a: float, b: float, c: float):
self.a = a
self.b = b
self.c = c
def accept(self, visitor: ShapeVisitor) -> None:
visitor.visit_triangle(self)
# Step 4: Create Concrete Visitors
class AreaCalculator(ShapeVisitor):
"""Visitor that calculates total area"""
def __init__(self):
self.total_area = 0.0
def visit_circle(self, circle: Circle) -> None:
area = math.pi * circle.radius ** 2
self.total_area += area
print(f" ⭕ Circle area: {area:.2f}")
def visit_rectangle(self, rectangle: Rectangle) -> None:
area = rectangle.width * rectangle.height
self.total_area += area
print(f" ⬜ Rectangle area: {area:.2f}")
def visit_triangle(self, triangle: Triangle) -> None:
s = (triangle.a + triangle.b + triangle.c) / 2
area = math.sqrt(s * (s-triangle.a) * (s-triangle.b) * (s-triangle.c))
self.total_area += area
print(f" 🔺 Triangle area: {area:.2f}")
def get_total(self) -> float:
return self.total_area
class PerimeterCalculator(ShapeVisitor):
"""Visitor that calculates total perimeter"""
def __init__(self):
self.total_perimeter = 0.0
def visit_circle(self, circle: Circle) -> None:
perimeter = 2 * math.pi * circle.radius
self.total_perimeter += perimeter
print(f" ⭕ Circle perimeter: {perimeter:.2f}")
def visit_rectangle(self, rectangle: Rectangle) -> None:
perimeter = 2 * (rectangle.width + rectangle.height)
self.total_perimeter += perimeter
print(f" ⬜ Rectangle perimeter: {perimeter:.2f}")
def visit_triangle(self, triangle: Triangle) -> None:
perimeter = triangle.a + triangle.b + triangle.c
self.total_perimeter += perimeter
print(f" 🔺 Triangle perimeter: {perimeter:.2f}")
def get_total(self) -> float:
return self.total_perimeter
class SVGExporter(ShapeVisitor):
"""Visitor that exports shapes to SVG"""
def __init__(self):
self.elements: List[str] = []
def visit_circle(self, circle: Circle) -> None:
svg = f'<circle cx="50" cy="50" r="{circle.radius}" fill="blue"/>'
self.elements.append(svg)
print(f" ⭕ Exported circle to SVG")
def visit_rectangle(self, rectangle: Rectangle) -> None:
svg = f'<rect x="10" y="10" width="{rectangle.width}" height="{rectangle.height}" fill="green"/>'
self.elements.append(svg)
print(f" ⬜ Exported rectangle to SVG")
def visit_triangle(self, triangle: Triangle) -> None:
svg = f'<polygon points="50,10 10,90 90,90" fill="red"/>'
self.elements.append(svg)
print(f" 🔺 Exported triangle to SVG")
def get_svg(self) -> str:
elements = "\n ".join(self.elements)
return f'<svg xmlns="http://www.w3.org/2000/svg">\n {elements}\n</svg>'
# Step 5: Use the pattern
def main():
print("=" * 60)
print("Shape Calculator (Visitor Pattern)")
print("=" * 60)
# Create shapes
shapes: List[Shape] = [
Circle(5),
Rectangle(4, 6),
Triangle(3, 4, 5),
Circle(3),
]
# Calculate areas
print("\n📐 Calculating Areas:")
print("-" * 40)
area_calc = AreaCalculator()
for shape in shapes:
shape.accept(area_calc)
print(f"\n 📊 Total Area: {area_calc.get_total():.2f}")
# Calculate perimeters
print("\n📏 Calculating Perimeters:")
print("-" * 40)
perimeter_calc = PerimeterCalculator()
for shape in shapes:
shape.accept(perimeter_calc)
print(f"\n 📊 Total Perimeter: {perimeter_calc.get_total():.2f}")
# Export to SVG
print("\n🎨 Exporting to SVG:")
print("-" * 40)
svg_exporter = SVGExporter()
for shape in shapes:
shape.accept(svg_exporter)
print(f"\n 📄 SVG Output:\n{svg_exporter.get_svg()}")
# Easy to add new operation - just create new visitor!
print("\n" + "=" * 60)
print("✅ Visitor Pattern: New operations without modifying shapes!")
print("✅ Adding new operation = Just one new visitor class!")
if __name__ == "__main__":
main()

Real-World Software Example: Document Exporter

Section titled “Real-World Software Example: Document Exporter”

Now let’s see a realistic software example - a document system that needs to export to multiple formats.

You’re building a document editor with different elements (headings, paragraphs, code blocks, images). You need to export to HTML, Markdown, PDF, and plain text. Without Visitor Pattern:

bad_document.py
# ❌ Without Visitor Pattern - Export methods in every element!
class Heading:
def __init__(self, text: str, level: int):
self.text = text
self.level = level
def to_html(self) -> str:
return f"<h{self.level}>{self.text}</h{self.level}>"
def to_markdown(self) -> str:
return f"{'#' * self.level} {self.text}"
def to_plain_text(self) -> str:
return self.text.upper()
# New format? Add method here AND in every other class!
class Paragraph:
def __init__(self, text: str):
self.text = text
def to_html(self) -> str:
return f"<p>{self.text}</p>"
def to_markdown(self) -> str:
return self.text
def to_plain_text(self) -> str:
return self.text
# New format? Add method here too!
class CodeBlock:
def __init__(self, code: str, language: str):
self.code = code
self.language = language
def to_html(self) -> str:
return f"<pre><code class='{self.language}'>{self.code}</code></pre>"
def to_markdown(self) -> str:
return f"```{self.language}\n{self.code}\n```"
def to_plain_text(self) -> str:
return self.code
# And here... for EVERY element type!
# Problems:
# - 4 export formats × 5 element types = 20 methods!
# - Adding PDF export = add method to ALL classes
# - Related export code (all HTML) spread everywhere

Problems:

  • Export methods scattered across all element classes
  • Adding new format requires modifying ALL classes
  • Related code (all HTML export) spread across files
  • Elements have too many responsibilities
classDiagram
    class DocumentElement {
        <<interface>>
        +accept(visitor) void
    }
    class Heading {
        -text: str
        -level: int
        +accept(visitor) void
    }
    class Paragraph {
        -text: str
        +accept(visitor) void
    }
    class CodeBlock {
        -code: str
        -language: str
        +accept(visitor) void
    }
    class DocumentVisitor {
        <<interface>>
        +visit_heading(heading) void
        +visit_paragraph(para) void
        +visit_code_block(code) void
    }
    class HTMLExporter {
        +visit_heading(heading) void
        +visit_paragraph(para) void
        +visit_code_block(code) void
        +get_output() str
    }
    class MarkdownExporter {
        +visit_heading(heading) void
        +visit_paragraph(para) void
        +visit_code_block(code) void
        +get_output() str
    }
    
    DocumentElement <|.. Heading : implements
    DocumentElement <|.. Paragraph : implements
    DocumentElement <|.. CodeBlock : implements
    DocumentVisitor <|.. HTMLExporter : implements
    DocumentVisitor <|.. MarkdownExporter : implements
    DocumentElement --> DocumentVisitor : accepts
visitor_document.py
from abc import ABC, abstractmethod
from typing import List
# Step 1: Define the Visitor interface
class DocumentVisitor(ABC):
"""Visitor interface for document operations"""
@abstractmethod
def visit_heading(self, heading: 'Heading') -> None:
pass
@abstractmethod
def visit_paragraph(self, paragraph: 'Paragraph') -> None:
pass
@abstractmethod
def visit_code_block(self, code_block: 'CodeBlock') -> None:
pass
@abstractmethod
def visit_image(self, image: 'Image') -> None:
pass
# Step 2: Define the Element interface
class DocumentElement(ABC):
"""Element interface - document elements accept visitors"""
@abstractmethod
def accept(self, visitor: DocumentVisitor) -> None:
pass
# Step 3: Create Concrete Elements
class Heading(DocumentElement):
"""Heading element"""
def __init__(self, text: str, level: int = 1):
self.text = text
self.level = min(max(level, 1), 6) # H1-H6
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_heading(self)
class Paragraph(DocumentElement):
"""Paragraph element"""
def __init__(self, text: str):
self.text = text
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_paragraph(self)
class CodeBlock(DocumentElement):
"""Code block element"""
def __init__(self, code: str, language: str = ""):
self.code = code
self.language = language
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_code_block(self)
class Image(DocumentElement):
"""Image element"""
def __init__(self, src: str, alt: str = ""):
self.src = src
self.alt = alt
def accept(self, visitor: DocumentVisitor) -> None:
visitor.visit_image(self)
# Step 4: Create Concrete Visitors
class HTMLExporter(DocumentVisitor):
"""Exports document to HTML"""
def __init__(self):
self.output: List[str] = []
def visit_heading(self, heading: Heading) -> None:
self.output.append(
f"<h{heading.level}>{heading.text}</h{heading.level}>"
)
def visit_paragraph(self, paragraph: Paragraph) -> None:
self.output.append(f"<p>{paragraph.text}</p>")
def visit_code_block(self, code_block: CodeBlock) -> None:
lang = f' class="language-{code_block.language}"' if code_block.language else ''
self.output.append(
f"<pre><code{lang}>{code_block.code}</code></pre>"
)
def visit_image(self, image: Image) -> None:
self.output.append(
f'<img src="{image.src}" alt="{image.alt}"/>'
)
def get_output(self) -> str:
return "\n".join(self.output)
class MarkdownExporter(DocumentVisitor):
"""Exports document to Markdown"""
def __init__(self):
self.output: List[str] = []
def visit_heading(self, heading: Heading) -> None:
self.output.append(f"{'#' * heading.level} {heading.text}")
def visit_paragraph(self, paragraph: Paragraph) -> None:
self.output.append(paragraph.text)
def visit_code_block(self, code_block: CodeBlock) -> None:
self.output.append(f"```{code_block.language}")
self.output.append(code_block.code)
self.output.append("```")
def visit_image(self, image: Image) -> None:
self.output.append(f"![{image.alt}]({image.src})")
def get_output(self) -> str:
return "\n\n".join(self.output)
class PlainTextExporter(DocumentVisitor):
"""Exports document to plain text"""
def __init__(self):
self.output: List[str] = []
def visit_heading(self, heading: Heading) -> None:
self.output.append(heading.text.upper())
self.output.append("=" * len(heading.text))
def visit_paragraph(self, paragraph: Paragraph) -> None:
self.output.append(paragraph.text)
def visit_code_block(self, code_block: CodeBlock) -> None:
self.output.append("--- CODE ---")
self.output.append(code_block.code)
self.output.append("--- END ---")
def visit_image(self, image: Image) -> None:
self.output.append(f"[Image: {image.alt}]")
def get_output(self) -> str:
return "\n\n".join(self.output)
class WordCounter(DocumentVisitor):
"""Counts words in the document"""
def __init__(self):
self.word_count = 0
self.code_lines = 0
self.image_count = 0
def visit_heading(self, heading: Heading) -> None:
self.word_count += len(heading.text.split())
def visit_paragraph(self, paragraph: Paragraph) -> None:
self.word_count += len(paragraph.text.split())
def visit_code_block(self, code_block: CodeBlock) -> None:
self.code_lines += len(code_block.code.split('\n'))
def visit_image(self, image: Image) -> None:
self.image_count += 1
def get_stats(self) -> dict:
return {
"words": self.word_count,
"code_lines": self.code_lines,
"images": self.image_count
}
# Step 5: Create a Document class to hold elements
class Document:
"""Document containing multiple elements"""
def __init__(self, title: str):
self.title = title
self.elements: List[DocumentElement] = []
def add(self, element: DocumentElement) -> 'Document':
self.elements.append(element)
return self
def accept(self, visitor: DocumentVisitor) -> None:
"""Let visitor visit all elements"""
for element in self.elements:
element.accept(visitor)
# Step 6: Use the pattern
def main():
print("=" * 60)
print("Document Exporter (Visitor Pattern)")
print("=" * 60)
# Create a document
doc = Document("My Article")
doc.add(Heading("Introduction to Design Patterns", 1))
doc.add(Paragraph("Design patterns are reusable solutions to common problems in software design."))
doc.add(Heading("Example Code", 2))
doc.add(CodeBlock("def hello():\n print('Hello, World!')", "python"))
doc.add(Paragraph("The code above demonstrates a simple function."))
doc.add(Image("diagram.png", "Class Diagram"))
# Export to HTML
print("\n📄 HTML Export:")
print("-" * 40)
html_exporter = HTMLExporter()
doc.accept(html_exporter)
print(html_exporter.get_output())
# Export to Markdown
print("\n📝 Markdown Export:")
print("-" * 40)
md_exporter = MarkdownExporter()
doc.accept(md_exporter)
print(md_exporter.get_output())
# Export to Plain Text
print("\n📃 Plain Text Export:")
print("-" * 40)
text_exporter = PlainTextExporter()
doc.accept(text_exporter)
print(text_exporter.get_output())
# Get document statistics
print("\n📊 Document Statistics:")
print("-" * 40)
counter = WordCounter()
doc.accept(counter)
stats = counter.get_stats()
print(f" Words: {stats['words']}")
print(f" Code Lines: {stats['code_lines']}")
print(f" Images: {stats['images']}")
print("\n" + "=" * 60)
print("✅ Visitor Pattern: New export format = Just one new visitor!")
print("✅ All HTML logic in HTMLExporter - easy to maintain!")
if __name__ == "__main__":
main()

Visitor Pattern uses double dispatch to determine which code to run:

graph LR
    A[element.accept visitor] -->|1st dispatch| B[concrete element type]
    B -->|2nd dispatch| C[visitor.visit_X element]
    C --> D[specific operation]
    
    style A fill:#f9f,stroke:#333
    style D fill:#9f9,stroke:#333
  1. First dispatch: Based on element type (Circle, Rectangle)
  2. Second dispatch: Based on visitor type (AreaCalculator, SVGExporter)

This allows choosing behavior based on BOTH types at runtime!


Mistake 1: Visitor Knows Too Much About Elements

Section titled “Mistake 1: Visitor Knows Too Much About Elements”
encapsulation.py
# ❌ Bad: Visitor accesses private internals
class BadVisitor:
def visit_circle(self, circle):
# Bad: Accessing private attributes!
area = 3.14 * circle._internal_radius ** 2
# ✅ Good: Use public getters
class GoodVisitor:
def visit_circle(self, circle):
# Good: Uses public interface
area = 3.14 * circle.get_radius() ** 2

Mistake 2: Forgetting to Add Visit Methods for New Elements

Section titled “Mistake 2: Forgetting to Add Visit Methods for New Elements”
missing_visit.py
# ❌ Bad: Adding new element without updating visitors
class Pentagon(Shape):
def accept(self, visitor):
visitor.visit_pentagon(self) # But visitors don't have this!
# ✅ Good: Use default implementation or abstract base
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle): pass
@abstractmethod
def visit_rectangle(self, rect): pass
@abstractmethod
def visit_pentagon(self, pentagon): pass # Add when adding Pentagon!

Mistake 3: Using Visitor When Structure Changes Often

Section titled “Mistake 3: Using Visitor When Structure Changes Often”
changing_structure.py
# ❌ Bad: Using visitor when elements change frequently
# If you add Triangle, Square, Pentagon, Hexagon...
# Every visitor must be updated!
# ✅ Better: Use polymorphism when structure changes often
class Shape(ABC):
@abstractmethod
def area(self) -> float: pass
@abstractmethod
def perimeter(self) -> float: pass
# Each shape implements its own operations
# Adding new shape = just one new class

  1. Open/Closed for Operations - Add operations without modifying elements
  2. Single Responsibility - Each visitor has one operation
  3. Related Code Together - All export logic in one class
  4. Accumulate State - Visitors can gather info across visits
  5. Double Dispatch - Operation based on both types
  6. Clean Elements - Elements don’t have operation logic

Visitor Pattern is a behavioral design pattern that lets you add new operations to objects without modifying them. It separates algorithms from the object structure they operate on.

  • Add operations - Without modifying element classes
  • Single Responsibility - Each visitor = one operation
  • Related code together - All HTML export in one class
  • Accumulate state - Gather info across visits
  • Double dispatch - Choose based on both types
  1. Define Visitor interface - Visit method per element type
  2. Define Element interface - accept(visitor) method
  3. Create Concrete Elements - Call visitor.visit_X(this)
  4. Create Concrete Visitors - Implement operation per type
  5. Client - Creates visitor, passes to elements
Element.accept(visitor) → visitor.visit_X(element)
  • Visitor - Interface with visit methods
  • Concrete Visitor - Implements specific operation
  • Element - Interface with accept method
  • Concrete Element - Calls appropriate visit method
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle): pass
@abstractmethod
def visit_rectangle(self, rect): pass
class Shape(ABC):
@abstractmethod
def accept(self, visitor): pass
class Circle(Shape):
def accept(self, visitor):
visitor.visit_circle(self) # Double dispatch!
class AreaCalculator(ShapeVisitor):
def visit_circle(self, circle):
return 3.14 * circle.radius ** 2

✅ Object structure is stable
✅ Operations change often
✅ Need type-specific operations
✅ Need to accumulate state

❌ Element types change often
❌ Few simple operations
❌ Don’t need type-specific handling

  • Visitor Pattern = Operations on object structures
  • Double Dispatch = Choose by both types
  • Visitor = One operation, many element types
  • Element = Accepts visitors
  • Trade-off = Easy to add operations, hard to add elements

What to say:

“Visitor Pattern is a behavioral design pattern that separates algorithms from the objects they operate on. It lets you add new operations to existing class hierarchies without modifying them. The key mechanism is double dispatch - the operation executed depends on both the element type AND the visitor type.”

Why it matters:

  • Shows you understand the fundamental purpose
  • Demonstrates knowledge of double dispatch
  • Indicates you understand the trade-offs

Must explain:

  • Single dispatch: Method called based on ONE type (receiver)
  • Double dispatch: Method called based on TWO types
  • How visitor achieves it: element.accept() → visitor.visit_X()

Example to give:

“Normal method calls use single dispatch - the method called depends on the object’s type. Visitor achieves double dispatch: first accept() is called on the element (first dispatch based on element type), then that calls visitor.visit_X(this) (second dispatch based on visitor type). This lets us choose behavior based on BOTH types.”

Must discuss:

  • Visitor - Multiple operations, multiple element types
  • Strategy - One operation, multiple algorithms
  • Key difference - Visitor separates algorithm from objects; Strategy swaps algorithms

Example to give:

Strategy Pattern is about swapping ONE algorithm - like choosing between QuickSort and MergeSort for sorting. Visitor Pattern is about performing MANY operations across MANY types - like exporting documents to HTML, Markdown, and PDF. Each visitor is an operation that handles all element types.”

Benefits to mention:

  • Easy to add operations - One new visitor class
  • Single Responsibility - Each visitor = one operation
  • Related code together - All HTML logic in one place
  • Accumulate state - Count, aggregate as you visit

Trade-offs to acknowledge:

  • Hard to add elements - Add visit method to ALL visitors
  • Breaking encapsulation - Visitors need element internals
  • Complexity - More classes, double dispatch

Q: “When would you NOT use Visitor Pattern?”

A:

“I wouldn’t use Visitor when the element hierarchy changes frequently. Every new element type requires adding a visit method to EVERY visitor - that’s the opposite of Open/Closed Principle. If you’re adding new shapes regularly but operations are stable, use polymorphism instead (each shape implements its own operations).”

Q: “How does Visitor Pattern handle the expression problem?”

A:

“The expression problem is the difficulty of adding both new types AND new operations in a type-safe way. Visitor Pattern solves half of it - it makes adding new operations easy (new visitor class), but makes adding new types hard (modify all visitors). For systems where operations change more than types, Visitor is perfect.”

Q: “Where is Visitor Pattern commonly used?”

A:

“Visitor is common in compilers and interpreters - the AST structure is stable but you need many operations: type checking, code generation, optimization, pretty printing. Document processors use it too - document elements are stable but you need HTML export, PDF export, word count, spell check, etc.”

Before your interview, make sure you can:

  • Define Visitor Pattern clearly
  • Explain double dispatch
  • Compare Visitor vs Strategy
  • Implement Visitor from scratch
  • Explain the trade-off (operations vs types)
  • List benefits and drawbacks
  • Give real-world examples (compilers, document exporters)
  • Discuss when NOT to use it
  • Draw the class structure

Remember: Visitor Pattern is about adding new operations without modifying classes - separate algorithms from the objects they operate on! 🎯