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.
Table of Contents
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.

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 DispatchPythonInstead 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)PythonExtending 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)PythonOutput
=== Calculating Areas ===
Circle Area: 78.5
Square Area: 16
=== Calculating Perimeters ===
Circle Perimeter: 31.4PythonNo 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
- Document Parsing: It facilitates the separation of concerns between document elements and processing operations.
- 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.
- 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. - 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. - 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
AnalyticsPythonWhen 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()
PythonAnd 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 methodsPythonBetter 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
CodeGenVisitorPythonEach 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
└ NetBankingPaymentPythonCode 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 scanPythonCommand 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 schedulersPythonTemplate 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()PythonTrick
- 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”