The Visitor Pattern is a behavioral design pattern that allows you to add new operations to existing classes without modifying their structure. In simple terms, the Visitor Pattern separates algorithms from the object structures they operate on.
\n\nCore Idea
\n\nImagine the scenario of a hospital physical examination: you (the visited element) remain still, while different doctors (visitors) come to examine you. Each doctor focuses on their own specialty, but they all examine the same you.
\n\nIn programming, the Visitor Pattern works similarly:
\n\n- \n
- The visited elements remain stable and unchanged \n
- Visitors can flexibly add new operations \n
- Elements accept visitors, allowing visitors to perform operations on them \n
\n\n
Why We Need the Visitor Pattern
\n\nLimitations of Traditional Approaches
\n\nSuppose we have a graphics system containing multiple shapes:
\n\nExamples
\n\nclass Circle:\n def __init__ (self, radius):\n self.radius= radius\n \n def area(self):\n return 3.14 * self.radius * self.radius\n \n def perimeter(self):\n return 2 * 3.14 * self.radius\n\nclass Rectangle:\n def __init__ (self, width, height):\n self.width= width\n self.height= height\n \n def area(self):\n return self.width * self.height\n \n def perimeter(self):\n return 2 * (self.width + self.height)\n\n\nThe problem: If we want to add new features, such as calculating the center of gravity of shapes, exporting to SVG format, etc., we need to modify each shape class. This violates the Open/Closed Principle (open for extension, closed for modification).
\n\nAdvantages of the Visitor Pattern
\n\nThe Visitor Pattern solves this problem in the following ways:
\n\n- \n
- Easy to add new operations: Just create a new visitor class without modifying existing element classes \n
- Related operations are centrally managed: Organize related operations within the same visitor \n
- Stable element structure: Element classes don't need frequent modifications \n
\n\n
Implementation of the Visitor Pattern
\n\nBasic Structure
\n\nLet's understand the implementation of the Visitor Pattern through a concrete example:
\n\nExamples
\n\nfrom abc import ABC, abstractmethod\n\n# 1. Define element interface\nclass ShapeElement(ABC):\n @abstractmethod\n def accept(self, visitor):\n pass\n\n# 2. Define visitor interface\nclass ShapeVisitor(ABC):\n @abstractmethod\n def visit_circle(self, circle):\n pass\n \n @abstractmethod\n def visit_rectangle(self, rectangle):\n pass\n\n# 3. Concrete element classes\nclass Circle(ShapeElement):\n def __init__ (self, radius):\n self.radius= radius\n \n def accept(self, visitor):\n visitor.visit_circle(self)\n\nclass Rectangle(ShapeElement):\n def __init__ (self, width, height):\n self.width= width\n self.height= height\n \n def accept(self, visitor):\n visitor.visit_rectangle(self)\n\n# 4. Concrete visitor classes\nclass AreaCalculator(ShapeVisitor):\n def visit_circle(self, circle):\n area =3.14 * circle.radius * circle.radius\n print(f"Circle area: {area:.2f}")\n return area\n \n def visit_rectangle(self, rectangle):\n area = rectangle.width * rectangle.height\n print(f"Rectangle area: {area:.2f}")\n return area\n\nclass PerimeterCalculator(ShapeVisitor):\n def visit_circle(self, circle):\n perimeter =2 * 3.14 * circle.radius\n print(f"Circle perimeter: {perimeter:.2f}")\n return perimeter\n \n def visit_rectangle(self, rectangle):\n perimeter =2 * (rectangle.width + rectangle.height)\n print(f"Rectangle perimeter: {perimeter:.2f}")\n return perimeter\n\n\nUsage Example
\n\nExamples
\n\n# Create shape objects\nshapes =[\n Circle(5),\n Rectangle(4,6),\n Circle(3)\n]\n\n# Create visitors\narea_calculator = AreaCalculator()\nperimeter_calculator = PerimeterCalculator()\n\nprint("=== Calculate area ===")\nfor shape in shapes:\n shape.accept(area_calculator)\n\nprint("\\n=== Calculate perimeter ===")\nfor shape in shapes:\n shape.accept(perimeter_calculator)\n\n\nOutput:
\n\n=== Calculate area ===\nCircle area: 78.50\nRectangle area: 24.00\nCircle area: 28.26\n=== Calculate perimeter ===\nCircle perimeter: 31.40\nRectangle perimeter: 20.00\nCircle perimeter: 18.84\n\n\n\n\n
Core Components of the Visitor Pattern
\n\n1. Visitor (Visitor Interface)
\n\nDefines visit methods for each concrete element class.
\n\nExamples
\n\nclass ShapeVisitor(ABC):\n def visit_circle(self, circle): pass\n def visit_rectangle(self, rectangle): pass\n def visit_triangle(self, triangle): pass # Can be easily extended\n\n\n2. ConcreteVisitor (Concrete Visitor)
\n\nImplements the operations defined in the visitor interface.
\n\nExamples
\n\nclass ExportVisitor(ShapeVisitor):\n def visit_circle(self, circle):\n return f'<circle r="{circle.radius}" />'\n \n def visit_rectangle(self, rectangle):\n return f'<rect width="{rectangle.width}" height="{rectangle.height}" />'\n\n\n3. Element (Element Interface)
\n\nDefines the method to accept a visitor.
\n\nExamples
\n\nclass ShapeElement(ABC):\n @abstractmethod\n def accept(self, visitor):\n pass\n\n\n4. ConcreteElement (Concrete Element)
\n\nImplements the element interface, calling the visitor's corresponding method in the accept method.
\n\nExamples
\n\nclass Circle(ShapeElement):\n def accept(self, visitor):\n visitor.visit_circle(self) # Double dispatch occurs here\n\n\n\n\n
Advanced Application Example
\n\nComplex Document Processing System
\n\nLet's look at a more practical example: processing different types of document elements.
\n\nExamples
\n\nfrom abc import ABC, abstractmethod\n\n# Document element interface\nclass DocumentElement(ABC):\n @abstractmethod\n def accept(self, visitor):\n pass\n\n# Concrete document elements\nclass Paragraph(DocumentElement):\n def __init__ (self, text):\n self.text= text\n \n def accept(self, visitor):\n return visitor.visit_paragraph(self)\n\nclass Heading(DocumentElement):\n def __init__ (self, text, level):\n self.text= text\n self.level= level\n \n def accept(self, visitor):\n return visitor.visit_heading(self)\n\nclass List(DocumentElement):\n def __init__ (self, items):\n self.items= items\n \n def accept(self, visitor):\n return visitor.visit_list(self)\n\n# Visitor interface\nclass DocumentVisitor(ABC):\n @abstractmethod\n def visit_paragraph(self, paragraph): pass\n \n @abstractmethod\n def visit_heading(self, heading): pass\n \n @abstractmethod\n def visit_list(self, list_element): pass\n\n# Concrete visitor: HTML export\nclass HTMLExportVisitor(DocumentVisitor):\n def visit_paragraph(self, paragraph):\n return f"<p>{paragraph.text}</p>"\n \n def visit_heading(self, heading):\n return f"<h{heading.level}>{heading.text}</h{heading.level}>"\n \n def visit_list(self, list_element):\n items ="".join(f"<li>{item}</li>"for item in list_element.items)\n return f"<ul>{items}</ul>"\n\n# Concrete visitor: Word count\nclass WordCountVisitor(DocumentVisitor):\n def __init__ (self):\n self.total_words=0\n \n def visit_paragraph(self, paragraph):\n words =len(paragraph.text.split())\n self.total_words += words\n return words\n \n def visit_heading(self, heading):\n words =len(heading.text.split())\n self.total_words += words\n return words\n \n def visit_list(self, list_element):\n words =sum(len(item.split())for item in list_element.items)\n self.total_words += words\n return words\n\n# Usage example\ndocument =[\n Heading("Python Tutorial",1),\n Paragraph("Python is a powerful programming language."),\n List(["List Item 1","List Item 2","List Item 3"]),\n Paragraph("Learning Python is fun.")\n]\n\n# HTML export\nhtml_visitor = HTMLExportVisitor()\nprint("=== HTML Export ===")\nfor element in document:\n print(element.accept(html_visitor))\n\n# Word count\nword_visitor = WordCountVisitor()\nprint("\\n=== Word count ===")\nfor element in document:\n words = element.accept(word_visitor)\n print(f"{element.__class__.__name__}: {words} words")\n\nprint(f"Total word count: {word_visitor.total_words}")\n\n\n\n\n
Pros and Cons of the Visitor Pattern
\n\nAdvantages
\n\n- \n
- Open/Closed Principle: Easy to add new operations without modifying existing classes \n
- Single Responsibility Principle: Concentrates related behaviors in a single visitor class \n
- Flexibility: Can choose different visitors at runtime \n
- Data separation: Algorithms are separated from data structures \n
Disadvantages
\n\n- \n
- Difficult to change element interfaces: Adding new element classes requires modifying all visitors \n
- May break encapsulation: Visitors may need access to private members of elements \n
- Complexity: May be overly complex for simple data structures \n
\n\n
Applicable Scenarios
\n\nScenarios Suitable for the Visitor Pattern
\n\n- \n
- Stable object structure: Need to define multiple operations on a relatively stable object structure \n
- Frequently changing operations: Need to frequently add new operations or algorithms \n
- Related operations centralized: Want to organize related operations together \n
- Complex object structure: Object structure contains many different types of objects \n
Real-world Application Cases
\n\n- \n
- Compilers: Various analyses on syntax trees (type checking, code optimization, etc.) \n
- Document processing: Different processing of document elements (export, statistics, formatting, etc.) \n
- GUI systems: Different operations on UI components (rendering, event handling, etc.) \n
- Game development: Different processing of game objects (collision detection, AI, etc.) \n
\n\n
Best Practices and Considerations
\n\n1. Using Double Dispatch
\n\nThe core of the Visitor Pattern is double dispatch:
\n\n- \n
- First dispatch: Element accepts visitor \n
- Second dispatch: Visitor visits concrete element \n
Examples
\n\n# In the element\ndef accept(self, visitor):\n visitor.visit_circle(self) # Here the specific visit method is determined\n\n\n2. Handling Visitor State
\n\nIf the visitor needs to maintain state:
\n\nExamples
\n\nclass StatisticsVisitor(ShapeVisitor):\n def __init__ (self):\n self.total_area=0\n self.shape_count=0\n \n def visit_circle(self, circle):\n area =3.14 * circle.radius * circle.radius\n self.total_area += area\n self.shape_count +=1\n \n def visit_rectangle(self, rectangle):\n area = rectangle.width * rectangle.height\n self.total_area += area\n self.shape_count +=1\n \n def get_statistics(self):\n return{\n 'total_area': self.total_area,\n 'shape_count': self.shape_count,\n 'average_area': self.total_area / self.shape_count if self.shape_count>0 else 0\n }\n\n\n3. Handling Visitor Exceptions
\n\nExamples
\n\nclass SafeVisitor(ShapeVisitor):\n def visit_circle(self, circle):\n try:\n # Perform operation\n pass\n except Exception as e:\n print(f"Error processing circle: {e}")\n \n def visit_rectangle(self, rectangle):\n try:\n # Perform operation\n pass\n except Exception as e:\n print(f"Error processing rectangle: {e}")\n\n\n\n\n
Summary
\n\nThe Visitor Pattern is a powerful design pattern that provides excellent extensibility by separating algorithms from data structures. Although it has certain complexity, it can significantly improve code maintainability and extensibility in appropriate scenarios.
\n\nKey points:
\n\n- \n
- The Visitor Pattern is suitable for scenarios where the object structure is stable but operations change frequently \n
- Polymorphic behavior is achieved through the double dispatch mechanism \n
- Easy to add new operations, but difficult to add new element types \n
- In actual projects, weigh its complexity against the benefits it brings \n
YouTip