Visitor Pattern
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.
Why Visitor Pattern?
Section titled “Why Visitor Pattern?”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.
What’s the Use of Visitor Pattern?
Section titled “What’s the Use of Visitor Pattern?”The Visitor Pattern is useful when:
- You need many unrelated operations - On objects in a structure
- Object structure rarely changes - But operations change often
- You want to avoid polluting classes - Keep operations separate
- You need operations across class hierarchy - Different handling per type
- 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:
- Add methods to every class - For each new operation
- Violate Single Responsibility - Classes do too much
- Violate Open/Closed - Modify classes for new operations
- Scatter related code - Export logic spread across classes
- Hard to add operations - Need to modify all classes
Simple Example: The Shape Calculator
Section titled “Simple Example: The Shape Calculator”Let’s start with a super simple example that anyone can understand!
Visual Representation
Section titled “Visual Representation”Double Dispatch Flow
Section titled “Double Dispatch Flow”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!
The Problem
Section titled “The Problem”You’re building a graphics application with different shapes. You need to calculate area, perimeter, and draw operations. Without Visitor Pattern:
# ❌ 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// ❌ Without Visitor Pattern - Operations in every class!
class Circle { private double radius;
public Circle(double radius) { this.radius = radius; }
public double area() { return Math.PI * radius * radius; }
public double perimeter() { return 2 * Math.PI * radius; }
public String draw() { return "Drawing circle with radius " + radius; }
// Adding new operation? Add method here! public String exportSvg() { return "<circle r=\"" + radius + "\"/>"; }}
class Rectangle { private double width, height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
public double area() { return width * height; }
public double perimeter() { return 2 * (width + height); }
public String draw() { return "Drawing rectangle " + width + "x" + height; }
// Adding new operation? Add method here too! public String exportSvg() { return "<rect width=\"" + width + "\" height=\"" + height + "\"/>"; }}
// 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 PrincipleProblems:
- Every new operation requires modifying ALL classes
- Related operation code scattered across files
- Classes have too many responsibilities
- Violates Open/Closed Principle
The Solution: Visitor Pattern
Section titled “The Solution: Visitor Pattern”Class Structure
Section titled “Class Structure”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"
from abc import ABC, abstractmethodimport mathfrom typing import List
# Step 1: Define the Visitor interfaceclass 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 interfaceclass 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 Elementsclass 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 Visitorsclass 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 patterndef 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()import java.util.*;
// Step 1: Define the Visitor interfaceinterface ShapeVisitor { /** * Visitor interface - defines visit methods for each shape type */ void visitCircle(Circle circle); void visitRectangle(Rectangle rectangle); void visitTriangle(Triangle triangle);}
// Step 2: Define the Element interfaceinterface Shape { /** * Element interface - shapes accept visitors */ void accept(ShapeVisitor visitor);}
// Step 3: Create Concrete Elementsclass Circle implements Shape { /** * Concrete element - circle shape */ private double radius;
public Circle(double radius) { this.radius = radius; }
public double getRadius() { return radius; }
@Override public void accept(ShapeVisitor visitor) { visitor.visitCircle(this); }}
class Rectangle implements Shape { /** * Concrete element - rectangle shape */ private double width; private double height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
public double getWidth() { return width; } public double getHeight() { return height; }
@Override public void accept(ShapeVisitor visitor) { visitor.visitRectangle(this); }}
class Triangle implements Shape { /** * Concrete element - triangle shape */ private double a, b, c;
public Triangle(double a, double b, double c) { this.a = a; this.b = b; this.c = c; }
public double getA() { return a; } public double getB() { return b; } public double getC() { return c; }
@Override public void accept(ShapeVisitor visitor) { visitor.visitTriangle(this); }}
// Step 4: Create Concrete Visitorsclass AreaCalculator implements ShapeVisitor { /** * Visitor that calculates total area */ private double totalArea = 0.0;
@Override public void visitCircle(Circle circle) { double area = Math.PI * circle.getRadius() * circle.getRadius(); totalArea += area; System.out.printf(" ⭕ Circle area: %.2f%n", area); }
@Override public void visitRectangle(Rectangle rectangle) { double area = rectangle.getWidth() * rectangle.getHeight(); totalArea += area; System.out.printf(" ⬜ Rectangle area: %.2f%n", area); }
@Override public void visitTriangle(Triangle triangle) { double s = (triangle.getA() + triangle.getB() + triangle.getC()) / 2; double area = Math.sqrt(s * (s - triangle.getA()) * (s - triangle.getB()) * (s - triangle.getC())); totalArea += area; System.out.printf(" 🔺 Triangle area: %.2f%n", area); }
public double getTotal() { return totalArea; }}
class PerimeterCalculator implements ShapeVisitor { /** * Visitor that calculates total perimeter */ private double totalPerimeter = 0.0;
@Override public void visitCircle(Circle circle) { double perimeter = 2 * Math.PI * circle.getRadius(); totalPerimeter += perimeter; System.out.printf(" ⭕ Circle perimeter: %.2f%n", perimeter); }
@Override public void visitRectangle(Rectangle rectangle) { double perimeter = 2 * (rectangle.getWidth() + rectangle.getHeight()); totalPerimeter += perimeter; System.out.printf(" ⬜ Rectangle perimeter: %.2f%n", perimeter); }
@Override public void visitTriangle(Triangle triangle) { double perimeter = triangle.getA() + triangle.getB() + triangle.getC(); totalPerimeter += perimeter; System.out.printf(" 🔺 Triangle perimeter: %.2f%n", perimeter); }
public double getTotal() { return totalPerimeter; }}
class SVGExporter implements ShapeVisitor { /** * Visitor that exports shapes to SVG */ private List<String> elements = new ArrayList<>();
@Override public void visitCircle(Circle circle) { String svg = String.format( "<circle cx=\"50\" cy=\"50\" r=\"%.0f\" fill=\"blue\"/>", circle.getRadius()); elements.add(svg); System.out.println(" ⭕ Exported circle to SVG"); }
@Override public void visitRectangle(Rectangle rectangle) { String svg = String.format( "<rect x=\"10\" y=\"10\" width=\"%.0f\" height=\"%.0f\" fill=\"green\"/>", rectangle.getWidth(), rectangle.getHeight()); elements.add(svg); System.out.println(" ⬜ Exported rectangle to SVG"); }
@Override public void visitTriangle(Triangle triangle) { String svg = "<polygon points=\"50,10 10,90 90,90\" fill=\"red\"/>"; elements.add(svg); System.out.println(" 🔺 Exported triangle to SVG"); }
public String getSvg() { String content = String.join("\n ", elements); return "<svg xmlns=\"http://www.w3.org/2000/svg\">\n " + content + "\n</svg>"; }}
// Step 5: Use the patternpublic class Main { public static void main(String[] args) { System.out.println("=".repeat(60)); System.out.println("Shape Calculator (Visitor Pattern)"); System.out.println("=".repeat(60));
// Create shapes List<Shape> shapes = Arrays.asList( new Circle(5), new Rectangle(4, 6), new Triangle(3, 4, 5), new Circle(3) );
// Calculate areas System.out.println("\n📐 Calculating Areas:"); System.out.println("-".repeat(40)); AreaCalculator areaCalc = new AreaCalculator(); for (Shape shape : shapes) { shape.accept(areaCalc); } System.out.printf("%n 📊 Total Area: %.2f%n", areaCalc.getTotal());
// Calculate perimeters System.out.println("\n📏 Calculating Perimeters:"); System.out.println("-".repeat(40)); PerimeterCalculator perimeterCalc = new PerimeterCalculator(); for (Shape shape : shapes) { shape.accept(perimeterCalc); } System.out.printf("%n 📊 Total Perimeter: %.2f%n", perimeterCalc.getTotal());
// Export to SVG System.out.println("\n🎨 Exporting to SVG:"); System.out.println("-".repeat(40)); SVGExporter svgExporter = new SVGExporter(); for (Shape shape : shapes) { shape.accept(svgExporter); } System.out.println("\n 📄 SVG Output:\n" + svgExporter.getSvg());
System.out.println("\n" + "=".repeat(60)); System.out.println("✅ Visitor Pattern: New operations without modifying shapes!"); System.out.println("✅ Adding new operation = Just one new visitor class!"); }}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.
The Problem
Section titled “The Problem”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:
# ❌ 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// ❌ Without Visitor Pattern - Export methods in every element!
class Heading { private String text; private int level;
public String toHtml() { return "<h" + level + ">" + text + "</h" + level + ">"; }
public String toMarkdown() { return "#".repeat(level) + " " + text; }
public String toPlainText() { return text.toUpperCase(); }
// New format? Add method here AND in every other class!}
class Paragraph { private String text;
public String toHtml() { return "<p>" + text + "</p>"; }
public String toMarkdown() { return text; }
public String toPlainText() { return text; }
// New format? Add method here too!}
// Problems:// - 4 export formats × 5 element types = 20 methods!// - Adding PDF export = add method to ALL classes// - Related export code (all HTML) spread everywhereProblems:
- 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
The Solution: Visitor Pattern
Section titled “The Solution: Visitor Pattern”Class Structure
Section titled “Class Structure”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
from abc import ABC, abstractmethodfrom typing import List
# Step 1: Define the Visitor interfaceclass 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 interfaceclass DocumentElement(ABC): """Element interface - document elements accept visitors"""
@abstractmethod def accept(self, visitor: DocumentVisitor) -> None: pass
# Step 3: Create Concrete Elementsclass 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 Visitorsclass 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"")
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 elementsclass 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 patterndef 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()import java.util.*;
// Step 1: Define the Visitor interfaceinterface DocumentVisitor { /** * Visitor interface for document operations */ void visitHeading(Heading heading); void visitParagraph(Paragraph paragraph); void visitCodeBlock(CodeBlock codeBlock); void visitImage(Image image);}
// Step 2: Define the Element interfaceinterface DocumentElement { /** * Element interface - document elements accept visitors */ void accept(DocumentVisitor visitor);}
// Step 3: Create Concrete Elementsclass Heading implements DocumentElement { private String text; private int level;
public Heading(String text, int level) { this.text = text; this.level = Math.min(Math.max(level, 1), 6); }
public String getText() { return text; } public int getLevel() { return level; }
@Override public void accept(DocumentVisitor visitor) { visitor.visitHeading(this); }}
class Paragraph implements DocumentElement { private String text;
public Paragraph(String text) { this.text = text; }
public String getText() { return text; }
@Override public void accept(DocumentVisitor visitor) { visitor.visitParagraph(this); }}
class CodeBlock implements DocumentElement { private String code; private String language;
public CodeBlock(String code, String language) { this.code = code; this.language = language; }
public String getCode() { return code; } public String getLanguage() { return language; }
@Override public void accept(DocumentVisitor visitor) { visitor.visitCodeBlock(this); }}
class Image implements DocumentElement { private String src; private String alt;
public Image(String src, String alt) { this.src = src; this.alt = alt; }
public String getSrc() { return src; } public String getAlt() { return alt; }
@Override public void accept(DocumentVisitor visitor) { visitor.visitImage(this); }}
// Step 4: Create Concrete Visitorsclass HTMLExporter implements DocumentVisitor { private List<String> output = new ArrayList<>();
@Override public void visitHeading(Heading heading) { output.add(String.format("<h%d>%s</h%d>", heading.getLevel(), heading.getText(), heading.getLevel())); }
@Override public void visitParagraph(Paragraph paragraph) { output.add(String.format("<p>%s</p>", paragraph.getText())); }
@Override public void visitCodeBlock(CodeBlock codeBlock) { String lang = codeBlock.getLanguage().isEmpty() ? "" : " class=\"language-" + codeBlock.getLanguage() + "\""; output.add(String.format("<pre><code%s>%s</code></pre>", lang, codeBlock.getCode())); }
@Override public void visitImage(Image image) { output.add(String.format("<img src=\"%s\" alt=\"%s\"/>", image.getSrc(), image.getAlt())); }
public String getOutput() { return String.join("\n", output); }}
class MarkdownExporter implements DocumentVisitor { private List<String> output = new ArrayList<>();
@Override public void visitHeading(Heading heading) { output.add("#".repeat(heading.getLevel()) + " " + heading.getText()); }
@Override public void visitParagraph(Paragraph paragraph) { output.add(paragraph.getText()); }
@Override public void visitCodeBlock(CodeBlock codeBlock) { output.add("```" + codeBlock.getLanguage()); output.add(codeBlock.getCode()); output.add("```"); }
@Override public void visitImage(Image image) { output.add(String.format("", image.getAlt(), image.getSrc())); }
public String getOutput() { return String.join("\n\n", output); }}
class WordCounter implements DocumentVisitor { private int wordCount = 0; private int codeLines = 0; private int imageCount = 0;
@Override public void visitHeading(Heading heading) { wordCount += heading.getText().split("\\s+").length; }
@Override public void visitParagraph(Paragraph paragraph) { wordCount += paragraph.getText().split("\\s+").length; }
@Override public void visitCodeBlock(CodeBlock codeBlock) { codeLines += codeBlock.getCode().split("\n").length; }
@Override public void visitImage(Image image) { imageCount++; }
public Map<String, Integer> getStats() { Map<String, Integer> stats = new HashMap<>(); stats.put("words", wordCount); stats.put("code_lines", codeLines); stats.put("images", imageCount); return stats; }}
// Step 5: Document classclass Document { private String title; private List<DocumentElement> elements = new ArrayList<>();
public Document(String title) { this.title = title; }
public Document add(DocumentElement element) { elements.add(element); return this; }
public void accept(DocumentVisitor visitor) { for (DocumentElement element : elements) { element.accept(visitor); } }}
// Step 6: Use the patternpublic class Main { public static void main(String[] args) { System.out.println("=".repeat(60)); System.out.println("Document Exporter (Visitor Pattern)"); System.out.println("=".repeat(60));
// Create a document Document doc = new Document("My Article"); doc.add(new Heading("Introduction to Design Patterns", 1)) .add(new Paragraph("Design patterns are reusable solutions.")) .add(new Heading("Example Code", 2)) .add(new CodeBlock("def hello():\n print('Hello!')", "python")) .add(new Image("diagram.png", "Class Diagram"));
// Export to HTML System.out.println("\n📄 HTML Export:"); System.out.println("-".repeat(40)); HTMLExporter htmlExporter = new HTMLExporter(); doc.accept(htmlExporter); System.out.println(htmlExporter.getOutput());
// Export to Markdown System.out.println("\n📝 Markdown Export:"); System.out.println("-".repeat(40)); MarkdownExporter mdExporter = new MarkdownExporter(); doc.accept(mdExporter); System.out.println(mdExporter.getOutput());
// Get statistics System.out.println("\n📊 Document Statistics:"); System.out.println("-".repeat(40)); WordCounter counter = new WordCounter(); doc.accept(counter); Map<String, Integer> stats = counter.getStats(); System.out.println(" Words: " + stats.get("words")); System.out.println(" Code Lines: " + stats.get("code_lines")); System.out.println(" Images: " + stats.get("images"));
System.out.println("\n✅ Visitor Pattern: New format = One new visitor!"); }}Visitor Pattern Concepts
Section titled “Visitor Pattern Concepts”Double Dispatch
Section titled “Double Dispatch”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
- First dispatch: Based on element type (Circle, Rectangle)
- Second dispatch: Based on visitor type (AreaCalculator, SVGExporter)
This allows choosing behavior based on BOTH types at runtime!
When to Use vs When Not
Section titled “When to Use vs When Not”Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”Mistake 1: Visitor Knows Too Much About Elements
Section titled “Mistake 1: Visitor Knows Too Much About Elements”# ❌ Bad: Visitor accesses private internalsclass BadVisitor: def visit_circle(self, circle): # Bad: Accessing private attributes! area = 3.14 * circle._internal_radius ** 2
# ✅ Good: Use public gettersclass GoodVisitor: def visit_circle(self, circle): # Good: Uses public interface area = 3.14 * circle.get_radius() ** 2// ❌ Bad: Visitor accesses private fieldsclass BadVisitor implements ShapeVisitor { @Override public void visitCircle(Circle circle) { // Bad: Would need reflection or package access double area = Math.PI * circle.radius * circle.radius; }}
// ✅ Good: Use public gettersclass GoodVisitor implements ShapeVisitor { @Override public void visitCircle(Circle circle) { // Good: Uses public interface double area = Math.PI * circle.getRadius() * circle.getRadius(); }}Mistake 2: Forgetting to Add Visit Methods for New Elements
Section titled “Mistake 2: Forgetting to Add Visit Methods for New Elements”# ❌ Bad: Adding new element without updating visitorsclass Pentagon(Shape): def accept(self, visitor): visitor.visit_pentagon(self) # But visitors don't have this!
# ✅ Good: Use default implementation or abstract baseclass 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!// ❌ Bad: Adding new element without updating interfaceclass Pentagon implements Shape { @Override public void accept(ShapeVisitor visitor) { visitor.visitPentagon(this); // Compile error! }}
// ✅ Good: Update visitor interface when adding elementsinterface ShapeVisitor { void visitCircle(Circle circle); void visitRectangle(Rectangle rectangle); void visitPentagon(Pentagon pentagon); // Add when adding Pentagon!}Mistake 3: Using Visitor When Structure Changes Often
Section titled “Mistake 3: Using Visitor When Structure Changes Often”# ❌ 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 oftenclass 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// ❌ 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 ofteninterface Shape { double area(); double perimeter();}
// Each shape implements its own operations// Adding new shape = just one new classBenefits of Visitor Pattern
Section titled “Benefits of Visitor Pattern”- Open/Closed for Operations - Add operations without modifying elements
- Single Responsibility - Each visitor has one operation
- Related Code Together - All export logic in one class
- Accumulate State - Visitors can gather info across visits
- Double Dispatch - Operation based on both types
- Clean Elements - Elements don’t have operation logic
Revision: Quick Catch-Up
Section titled “Revision: Quick Catch-Up”What is Visitor Pattern?
Section titled “What is Visitor Pattern?”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.
Why Use It?
Section titled “Why Use It?”- ✅ 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
How It Works?
Section titled “How It Works?”- Define Visitor interface - Visit method per element type
- Define Element interface - accept(visitor) method
- Create Concrete Elements - Call visitor.visit_X(this)
- Create Concrete Visitors - Implement operation per type
- Client - Creates visitor, passes to elements
Key Components
Section titled “Key Components”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
Simple Example
Section titled “Simple Example”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 ** 2When to Use?
Section titled “When to Use?”✅ Object structure is stable
✅ Operations change often
✅ Need type-specific operations
✅ Need to accumulate state
When NOT to Use?
Section titled “When NOT to Use?”❌ Element types change often
❌ Few simple operations
❌ Don’t need type-specific handling
Key Takeaways
Section titled “Key Takeaways”- 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
Interview Focus: Visitor Pattern
Section titled “Interview Focus: Visitor Pattern”Key Points to Remember
Section titled “Key Points to Remember”1. Core Concept
Section titled “1. Core Concept”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
2. Double Dispatch
Section titled “2. Double Dispatch”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.”
4. Trade-offs
Section titled “4. Trade-offs”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
5. Common Interview Questions
Section titled “5. Common Interview Questions”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.”
Interview Checklist
Section titled “Interview Checklist”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! 🎯