Python Visitor Design Pattern

1. Definition

The Visitor Design Pattern is a behavioral design pattern that allows adding further operations to objects without having to modify them. It involves two types of objects: Elements that have a "visit" method, and Visitors with a method for each Element they can visit.

2. Problem Statement

Imagine you have a collection of diverse objects, and you need to perform unrelated operations on these objects. It's not ideal to add all these operations to the objects, as it clutters their classes with unrelated behaviors and violates the Single Responsibility Principle.

3. Solution

The Visitor pattern suggests placing the new behavior into separate classes called visitors, by creating a visitor interface with visit methods for every type of object that can be passed as an argument. The objects then accept any visitor and direct the request to the appropriate visit method.

4. Real-World Use Cases

1. A shopping cart where various types of items need different types of tax calculations.

2. A document editor with multiple elements (text, images) that need different rendering methods.

5. Implementation Steps

1. Declare a visitor interface with a visit method for each concrete element class.

2. Each element class must have an "accept" method that takes a visitor as an argument.

3. Each concrete visitor class implements the visitor interface, defining how a type of visitor reacts to every type of element.

6. Implementation in Python

# Element interface
class Element:
    def accept(self, visitor):
        pass
# Concrete element classes
class ElementA(Element):
    def accept(self, visitor):
        visitor.visit_element_a(self)
    def operation_a(self):
        return "Element A operation"
class ElementB(Element):
    def accept(self, visitor):
        visitor.visit_element_b(self)
    def operation_b(self):
        return "Element B operation"
# Visitor interface
class Visitor:
    def visit_element_a(self, element):
        pass
    def visit_element_b(self, element):
        pass
# Concrete visitor classes
class ConcreteVisitor1(Visitor):
    def visit_element_a(self, element):
        print(f"Visitor 1: {element.operation_a()}")
    def visit_element_b(self, element):
        print(f"Visitor 1: {element.operation_b()}")
class ConcreteVisitor2(Visitor):
    def visit_element_a(self, element):
        print(f"Visitor 2: {element.operation_a()}")
    def visit_element_b(self, element):
        print(f"Visitor 2: {element.operation_b()}")
# Client code
elements = [ElementA(), ElementB()]
visitor1 = ConcreteVisitor1()
visitor2 = ConcreteVisitor2()
for element in elements:
    element.accept(visitor1)
    element.accept(visitor2)

Output:

Visitor 1: Element A operation
Visitor 2: Element A operation
Visitor 1: Element B operation
Visitor 2: Element B operation

Explanation:

1. Element provides an interface with an "accept" method.

2. ElementA and ElementB are concrete implementations of the Element and have their specific behaviors defined.

3. The Visitor interface declares methods corresponding to every concrete element class.

4. ConcreteVisitor1 and ConcreteVisitor2 define how they react to element classes.

5. In the client code, each element accepts a visitor which, in turn, "visits" the element.

7. When to use?

Use the Visitor pattern when:

1. You need to perform operations across a set of disparate objects.

2. Many distinct and unrelated operations are required, and it's undesirable to modify classes when new operations are added.

3. Objects are part of a complex class hierarchy, and you want to apply behaviors only to specific classes of the hierarchy.


Comments