Unravelling the Mysteries of the Visitor Design Pattern

Visitor Design Pattern is a powerful and flexible pattern that allows you to define operations on an object structure without altering its classes. Let’s embark on a journey to understand the intricacies of the Visitor pattern and how it can enhance the flexibility and extensibility of your code.

What?

The Visitor design pattern is a behavioural design pattern that allows you to add new behaviours to a group of classes without modifying their code. It provides a way to separate algorithms or operations from the object on which they operate.

visitor design pattern

Code

from abc import ABC, abstractmethod

# Step 1: Define the Visitor Interface
class ShapeVisitor(ABC):
    @abstractmethod
    def visit_circle(self, circle):
        pass

    @abstractmethod
    def visit_square(self, square):
        pass

# Step 2: Create a Concrete Visitor
class AreaCalculator(ShapeVisitor):
    """Calculates area of shapes"""
    def visit_circle(self, circle):
        area = 3.14 * circle.radius ** 2
        print(f"Circle Area: {area}")

    def visit_square(self, square):
        area = square.side ** 2
        print(f"Square Area: {area}")

# Step 3: Define the Element Interface
class Shape(ABC):
    @abstractmethod
    def accept(self, visitor: ShapeVisitor):
        pass

# Step 4: Create Concrete Elements
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def accept(self, visitor: ShapeVisitor):
        visitor.visit_circle(self) #2nd Dispatch

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def accept(self, visitor: ShapeVisitor):
        visitor.visit_square(self) #2nd Dispatch

# Step 5: Use the Visitor Pattern
if __name__ == "__main__":
    shapes = [Circle(5), Square(4)]
    visitor = AreaCalculator()

    for shape in shapes:
        shape.accept(visitor) # First Dispatch
Python

Instead of adding new methods to Circle and Square, we define a separate class (Visitor) that handles those operations.

  • Instead of adding an area() method inside Circle and Square, we define a visitor (AreaCalculator) that processes them.
  • This keeps the Shape classes clean and open for extension without modification.

Double Dispatch Mechanism

  • The first dispatch happens when we call shape.accept(visitor), which directs the call to Circle or Square.
  • The second dispatch happens when Circle. accept(visitor) calls visitor.visit_circle(self), dynamically resolving which method to execute.

Simplified trace

square.accept(visitor)  
# Calls -> visitor.visit_square(self)  (self = Square instance)
# Calls -> visit_square(self, square)  (square = passed Square instance)
Python

Extending our code for PerimeterCalculator

from abc import ABC, abstractmethod

# Step 1: Define the Visitor Interface
class ShapeVisitor(ABC):
    @abstractmethod
    def visit_circle(self, circle):
        pass

    @abstractmethod
    def visit_square(self, square):
        pass

# Step 2: Create Concrete Visitors
class AreaCalculator(ShapeVisitor):
    """Calculates area of shapes"""
    def visit_circle(self, circle):
        area = 3.14 * circle.radius ** 2
        print(f"Circle Area: {area}")

    def visit_square(self, square):
        area = square.side ** 2
        print(f"Square Area: {area}")

class PerimeterCalculator(ShapeVisitor):
    """Calculates perimeter of shapes"""
    def visit_circle(self, circle):
        perimeter = 2 * 3.14 * circle.radius
        print(f"Circle Perimeter: {perimeter}")

    def visit_square(self, square):
        perimeter = 4 * square.side
        print(f"Square Perimeter: {perimeter}")

# Step 3: Define the Element Interface
class Shape(ABC):
    @abstractmethod
    def accept(self, visitor: ShapeVisitor):
        pass

# Step 4: Create Concrete Elements
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def accept(self, visitor: ShapeVisitor):
        visitor.visit_circle(self)

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def accept(self, visitor: ShapeVisitor):
        visitor.visit_square(self)

# Step 5: Use the Visitor Pattern
if __name__ == "__main__":
    shapes = [Circle(5), Square(4)]
    
    area_visitor = AreaCalculator()
    perimeter_visitor = PerimeterCalculator()

    print("=== Calculating Areas ===")
    for shape in shapes:
        shape.accept(area_visitor)

    print("\n=== Calculating Perimeters ===")
    for shape in shapes:
        shape.accept(perimeter_visitor)
Python

Output

=== Calculating Areas ===
Circle Area: 78.5
Square Area: 16

=== Calculating Perimeters ===
Circle Perimeter: 31.4
Python

No Changes to Circle or Square

  • We added PerimeterCalculator without modifying Circle or Square.
  • If we were using a traditional approach, we’d have to modify the shape classes every time we added a new operation.

Use Case of the Visitor Design Pattern

  1. Document Parsing: It facilitates the separation of concerns between document elements and processing operations.
  2. GUI Components: Using the Visitor pattern, one can create visitor classes for each operation, ensuring that new functionalities can be seamlessly added without altering individual UI component classes. This promotes a clean separation of concerns and supports the open/closed principle.
  3. File System Processing:
    In file system applications with diverse file types, such as documents, images, and videos, the Visitor pattern streamlines the addition of processing operations. Whether it’s indexing, compression, or encryption, creating visitor classes for each operation ensures that new functionalities can be introduced without modifying the existing file classes. This maintains a flexible and scalable design.
  4. Game Development:
    Game development scenarios benefit from the Visitor Design Pattern when dealing with different game objects like characters, enemies, and obstacles. By implementing visitor classes for rendering, collision detection, and AI behaviour, developers can seamlessly integrate new features into game objects without altering their classes. This approach supports the dynamic and evolving nature of game development.
  5. Medical Information System:
    In medical information systems, where patient records encompass diverse data types, the Visitor pattern enables the performance of operations like data analysis, report generation, and data export. Creating visitor classes for each operation allows for the seamless addition of new processing capabilities, ensuring that the system remains adaptable and extensible to evolving requirements.

Benefits of the Visitor Design Pattern

  • Separation of Concerns: It promotes a separation between algorithms and the elements they operate on. By encapsulating operations in dedicated visitor classes, developers can focus on specific concerns within their code. This separation enhances readability and maintainability, allowing changes or additions to be made without affecting the core structure of the elements being visited.
  • Open/Closed Principle: It facilitates the extension of code without modifying existing classes. New functionalities can be introduced by creating new visitor classes, ensuring that the existing code remains closed for modification. This principle encourages a modular and scalable design, where the system can evolve by adding new features through extensions rather than alterations.

Why this complexity

The Visitor Pattern is not always worth using. They add complexity and should only be used when they provide real benefits.

When the Visitor Pattern is Worth Using

Use it when you have:

  • Fixed data structure, but frequent new operations
    • Example: A graphics editor has 20+ shape classes (Circle, Square, Triangle, etc.)
    • You often need to perform different operations on them:
      • Area
      • Perimeter
      • Export to SVG
      • Export to JSON
      • Hit detection
    • Instead of modifying each shape every time (violating OCP), you just add a new visitor.
  • You want to separate behaviour from data
    • The visitor moves logic out of the data objects.
    • Makes it easier to unit test, extend, and maintain.

When Not Worth It

Visitor is overkill if:

  • You only have a few classes and rarely add new operations.
  • You frequently add new types (e.g., new shapes). The visitor becomes a pain because every visitor must be updated.
  • Your logic is simple and can just be done with if isinstance() or polymorphism.

When to Use a Visitor

Object types are stable, but Operations keep increasing

Object types are stable
File
Directory
Link


Operations keep increasing
Size calculation
Search
Backup
Virus scan
Compression
Analytics
Python

When NOT to Use a Visitor

When You Have Many Objects and Many Operations

objects:
Car
Bike
Truck
Bus
Train
Plane
Ship

Operations:
start()
stop()
fuel()
repair()
insurance()
inspection()
clean()

With Visitor you would need:
Visitor interface

visitCar()
visitBike()
visitTruck()
visitBus()
visitTrain()
visitPlane()
visitShip()
Python

And every visitor must implement all methods. If you add a new object: Helicopter

You must modify every visitor. This becomes a maintenance nightmare.

A visitor causes combinatorial growth.

N object types
M operations
Visitor creates: N × M methods

Example 10 objects × 10 operations = 100 methods
Python

Better Patterns in This Case

  • Strategy Pattern
  • Command Pattern
  • etc

Avoid the visitor when:

  • Object types change frequently: Adding a new object requires updating all visitors.
  • Small number of operations: Visitor becomes over-engineering.
  • Simple systems: Polymorphism is simpler.

Real-life examples

Compiler

In a compiler, code is converted into an AST (Abstract Syntax Tree).

Example node types:

  • NumberNode
  • AddNode
  • MultiplyNode
  • VariableNode

Now you want different operations:

  • Evaluate expression
  • Print expression
  • Optimize expression
  • Generate bytecode

Instead of adding these methods to every node class, we create Visitors.

Example visitors:

EvaluateVisitor
PrintVisitor
OptimizationVisitor
CodeGenVisitor
Python

Each visitor performs a different operation on the same nodes.

File System Processing

Imagine a file system:

  • File
  • Directory
  • SymbolicLink

Now operations may be:

  • Calculate size
  • Search file
  • Compress files
  • Backup files

Instead of adding methods everywhere:

  • class File
  • class Directory

You create visitors:

  • SizeVisitor
  • SearchVisitor
  • BackupVisitor

Strategy, Visitor, Command, and Template Method

Strategy Pattern

Use when you want to switch algorithms/behaviours at runtime.

Key idea: Behaviour is interchangeable.

Rule: Same operation, multiple algorithms.

Example

PaymentStrategy
 ├ CreditCardPayment
 ├ UpiPayment
 └ NetBankingPayment
Python

Code chooses a strategy dynamically.

Visitor Pattern

Use when you want to add new operations to existing objects without modifying them.

Rule: Few object types, many operations.

Each operation becomes a Visitor.

Example:

objects:
File
Directory

Operations:
Size calculation
Search
Backup
Virus scan
Python

Command Pattern

Use when you want to encapsulate an action as an object.

Rule: Turn a request/action into an object.

Example:

Command
 ├ TurnOnLightCommand
 ├ TurnOffLightCommand
 ├ OpenDoorCommand
 └ CloseDoorCommand
 
 
 
Useful for
Undo/redo
Task queues
Remote controls
Job schedulers
Python

Template Method Pattern

Use when the algorithm structure is fixed, but some steps vary.

Different implementations override steps.

Rule: Same algorithm structure, customizable steps.

Example:

DataProcessing
 ├ readData()
 ├ processData()
 └ saveData()
Python

Trick

  • Do behaviours change at runtime? → Strategy
  • Do I add operations to existing objects? → Visitor
  • Do I represent actions as objects? → Command
  • Do I fix the algorithm flow but vary the steps? → Template Method

Conclusion

We hope you’ve gained insights into its elegance and practicality as we conclude our exploration of the Visitor Design Pattern. By embracing the separation of concerns and promoting extensibility, the Visitor pattern stands as a valuable tool in the arsenal of any software designer. Happy coding!

Resources

For further exploration, make sure to check out these helpful resources:

1 thought on “Unravelling the Mysteries of the Visitor Design Pattern”

Leave a Comment