Visitor
The Visitor design pattern is a behavioral design pattern that allows you to add further operations to objects without modifying them. This pattern follows the open/closed principle, allowing you to add new functionality to classes without changing their structure. It involves four main components:
- Visitor: An interface or abstract class for all visitors. It declares a visit method for each type of element that can be visited.
- Concrete Visitor: Classes that implement the Visitor interface. They provide the implementation for the visit methods.
- Element: An interface or abstract class for the elements that can be visited. It declares an accept method that takes a visitor as an argument.
- Concrete Element: Classes that implement the Element interface. They provide the implementation for the accept method.
When to Use
- When you need to perform many unrelated operations on objects without modifying their classes.
- When the object structure is fixed, and new operations need to be added frequently.
- When extending functionality by adding new methods to existing classes would be impractical.
Example Implementation in Python
Let’s create an example where we have a set of different shapes (Circle and Rectangle), and we want to perform various operations on them, such as calculating the area and drawing them, without modifying the shape classes.
from abc import ABC, abstractmethod
from math import pi
# Visitor interface
class Visitor(ABC):
@abstractmethod
def visit_circle(self, circle):
pass
@abstractmethod
def visit_rectangle(self, rectangle):
pass
# Concrete Visitor for calculating the area
class AreaVisitor(Visitor):
def visit_circle(self, circle):
return pi * (circle.radius ** 2)
def visit_rectangle(self, rectangle):
return rectangle.width * rectangle.height
# Concrete Visitor for drawing shapes
class DrawVisitor(Visitor):
def visit_circle(self, circle):
print(f"Drawing a circle with radius {circle.radius}")
def visit_rectangle(self, rectangle):
print(f"Drawing a rectangle with width {rectangle.width} and height {rectangle.height}")
# Element interface
class Shape(ABC):
@abstractmethod
def accept(self, visitor):
pass
# Concrete Element for Circle
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def accept(self, visitor):
return visitor.visit_circle(self)
# Concrete Element for Rectangle
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def accept(self, visitor):
return visitor.visit_rectangle(self)
# Client code
if __name__ == "__main__":
shapes = [
Circle(5),
Rectangle(3, 4)
]
area_visitor = AreaVisitor()
draw_visitor = DrawVisitor()
for shape in shapes:
area = shape.accept(area_visitor)
print(f"The area is {area}")
shape.accept(draw_visitor)
1. File System Operations
In a file system, you have different types of files (e.g., text files, images, videos) and you may want to perform various operations on these files, such as compression, encryption, or displaying properties. The Visitor pattern allows you to add these operations without modifying the file classes.
from abc import ABC, abstractmethod
# Visitor interface
class FileVisitor(ABC):
@abstractmethod
def visit_text_file(self, text_file):
pass
@abstractmethod
def visit_image_file(self, image_file):
pass
@abstractmethod
def visit_video_file(self, video_file):
pass
# Concrete Visitor for displaying properties
class PropertiesVisitor(FileVisitor):
def visit_text_file(self, text_file):
print(f"Text file: {text_file.name}, Size: {text_file.size} KB")
def visit_image_file(self, image_file):
print(f"Image file: {image_file.name}, Resolution: {image_file.resolution}")
def visit_video_file(self, video_file):
print(f"Video file: {video_file.name}, Duration: {video_file.duration} minutes")
# File interface
class File(ABC):
@abstractmethod
def accept(self, visitor):
pass
# Concrete Elements
class TextFile(File):
def __init__(self, name, size):
self.name = name
self.size = size
def accept(self, visitor):
visitor.visit_text_file(self)
class ImageFile(File):
def __init__(self, name, resolution):
self.name = name
self.resolution = resolution
def accept(self, visitor):
visitor.visit_image_file(self)
class VideoFile(File):
def __init__(self, name, duration):
self.name = name
self.duration = duration
def accept(self, visitor):
visitor.visit_video_file(self)
# Client code
if __name__ == "__main__":
files = [
TextFile("Document.txt", 120),
ImageFile("Picture.jpg", "1920x1080"),
VideoFile("Movie.mp4", 120)
]
properties_visitor = PropertiesVisitor()
for file in files:
file.accept(properties_visitor)
2. Shopping Cart with Different Discount Strategies
In a shopping cart system, you might have different types of items like electronics, groceries, and clothing, each of which might have different discount strategies. The Visitor pattern allows you to apply these discounts without modifying the item classes.
from abc import ABC, abstractmethod
# Visitor interface
class DiscountVisitor(ABC):
@abstractmethod
def visit_electronics(self, electronics):
pass
@abstractmethod
def visit_groceries(self, groceries):
pass
@abstractmethod
def visit_clothing(self, clothing):
pass
# Concrete Visitor for calculating discounts
class PercentageDiscountVisitor(DiscountVisitor):
def visit_electronics(self, electronics):
return electronics.price * 0.9 # 10% discount
def visit_groceries(self, groceries):
return groceries.price * 0.95 # 5% discount
def visit_clothing(self, clothing):
return clothing.price * 0.8 # 20% discount
# Item interface
class Item(ABC):
@abstractmethod
def accept(self, visitor):
pass
# Concrete Elements
class Electronics(Item):
def __init__(self, name, price):
self.name = name
self.price = price
def accept(self, visitor):
return visitor.visit_electronics(self)
class Groceries(Item):
def __init__(self, name, price):
self.name = name
self.price = price
def accept(self, visitor):
return visitor.visit_groceries(self)
class Clothing(Item):
def __init__(self, name, price):
self.name = name
self.price = price
def accept(self, visitor):
return visitor.visit_clothing(self)
# Client code
if __name__ == "__main__":
cart = [
Electronics("Laptop", 1000),
Groceries("Apple", 2),
Clothing("Jacket", 100)
]
discount_visitor = PercentageDiscountVisitor()
for item in cart:
discounted_price = item.accept(discount_visitor)
print(f"Discounted price for {item.name}: {discounted_price}")
3. Syntax Tree Evaluation
In a compiler or interpreter, you often work with abstract syntax trees (ASTs) representing different language constructs. The Visitor pattern can be used to traverse and evaluate these trees.
from abc import ABC, abstractmethod
# Visitor interface
class NodeVisitor(ABC):
@abstractmethod
def visit_number_node(self, number_node):
pass
@abstractmethod
def visit_add_node(self, add_node):
pass
@abstractmethod
def visit_subtract_node(self, subtract_node):
pass
# Concrete Visitor for evaluating the expression
class Evaluator(NodeVisitor):
def visit_number_node(self, number_node):
return number_node.value
def visit_add_node(self, add_node):
return add_node.left.accept(self) + add_node.right.accept(self)
def visit_subtract_node(self, subtract_node):
return subtract_node.left.accept(self) - subtract_node.right.accept(self)
# Node interface
class Node(ABC):
@abstractmethod
def accept(self, visitor):
pass
# Concrete Elements
class NumberNode(Node):
def __init__(self, value):
self.value = value
def accept(self, visitor):
return visitor.visit_number_node(self)
class AddNode(Node):
def __init__(self, left, right):
self.left = left
self.right = right
def accept(self, visitor):
return visitor.visit_add_node(self)
class SubtractNode(Node):
def __init__(self, left, right):
self.left = left
self.right = right
def accept(self, visitor):
return visitor.visit_subtract_node(self)
# Client code
if __name__ == "__main__":
# Constructing an AST for the expression (5 + 3) - 2
tree = SubtractNode(
AddNode(
NumberNode(5),
NumberNode(3)
),
NumberNode(2)
)
evaluator = Evaluator()
result = tree.accept(evaluator)
print(f"The result of the expression is: {result}")
Explanation of Examples
- File System Operations: Different file types (text, image, video) can have various operations performed on them (displaying properties). The Visitor pattern allows adding these operations without modifying the file classes.
- Shopping Cart with Different Discount Strategies: Different item types (electronics, groceries, clothing) can have different discount strategies. The Visitor pattern allows applying these strategies without modifying the item classes.
- Syntax Tree Evaluation: Different nodes in an abstract syntax tree (number, add, subtract) can be evaluated. The Visitor pattern allows adding new operations on the syntax tree nodes without modifying their structure.
Benefits
- Extensibility: New operations can be added without changing the object structure.
- Single Responsibility Principle: Different operations are separated into different classes.
- Open/Closed Principle: Classes are open for extension but closed for modification.
Drawbacks
- Complexity: Adding a new type of element requires adding a new visit method to the Visitor interface and all its concrete implementations.
- Coupling: The pattern introduces tight coupling between the Element and Visitor hierarchies.
The Visitor design pattern is particularly useful in scenarios where you need to perform various unrelated operations on a set of objects with a stable structure, making it a powerful tool for extending functionality in a clean and maintainable way.