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.

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