Python 访问者模式
访问者模式是一种行为设计模式,它允许你在不修改已有类结构的情况下,为这些类添加新的操作。简单来说,访问者模式将算法与其所操作的对象结构分离。
核心思想
想象一下你去医院体检的场景:你(被访问者)保持不动,而不同的医生(访问者)会来到你面前进行检查。每个医生都专注于自己的专业领域,但检查的都是同一个你。
在编程中,访问者模式的工作方式类似:
- 被访问的元素保持稳定不变
- 访问者可以灵活地添加新的操作
- 元素接受访问者的访问,让访问者对自己执行操作
为什么需要访问者模式
传统方式的局限性
假设我们有一个图形系统,包含多种形状:
实例
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
def perimeter(self):
return 2 * 3.14 * self.radius
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
def perimeter(self):
return 2 * 3.14 * self.radius
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
问题来了:如果我们想要添加新的功能,比如计算图形的重心、导出为SVG格式等,就需要修改每个形状类。这违反了开闭原则(对扩展开放,对修改关闭)。
访问者模式的优势
访问者模式通过以下方式解决这个问题:
- 易于添加新操作:只需创建新的访问者类,无需修改现有元素类
- 相关操作集中管理:将相关操作组织在同一个访问者中
- 元素结构稳定:元素类不需要频繁修改
访问者模式的实现
基本结构
让我们通过一个具体的例子来理解访问者模式的实现:
实例
from abc import ABC, abstractmethod
# 1. 定义元素接口
class ShapeElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# 2. 定义访问者接口
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle):
pass
@abstractmethod
def visit_rectangle(self, rectangle):
pass
# 3. 具体元素类
class Circle(ShapeElement):
def __init__(self, radius):
self.radius = radius
def accept(self, visitor):
visitor.visit_circle(self)
class Rectangle(ShapeElement):
def __init__(self, width, height):
self.width = width
self.height = height
def accept(self, visitor):
visitor.visit_rectangle(self)
# 4. 具体访问者类
class AreaCalculator(ShapeVisitor):
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
print(f"圆的面积: {area:.2f}")
return area
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
print(f"矩形的面积: {area:.2f}")
return area
class PerimeterCalculator(ShapeVisitor):
def visit_circle(self, circle):
perimeter = 2 * 3.14 * circle.radius
print(f"圆的周长: {perimeter:.2f}")
return perimeter
def visit_rectangle(self, rectangle):
perimeter = 2 * (rectangle.width + rectangle.height)
print(f"矩形的周长: {perimeter:.2f}")
return perimeter
# 1. 定义元素接口
class ShapeElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# 2. 定义访问者接口
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle):
pass
@abstractmethod
def visit_rectangle(self, rectangle):
pass
# 3. 具体元素类
class Circle(ShapeElement):
def __init__(self, radius):
self.radius = radius
def accept(self, visitor):
visitor.visit_circle(self)
class Rectangle(ShapeElement):
def __init__(self, width, height):
self.width = width
self.height = height
def accept(self, visitor):
visitor.visit_rectangle(self)
# 4. 具体访问者类
class AreaCalculator(ShapeVisitor):
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
print(f"圆的面积: {area:.2f}")
return area
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
print(f"矩形的面积: {area:.2f}")
return area
class PerimeterCalculator(ShapeVisitor):
def visit_circle(self, circle):
perimeter = 2 * 3.14 * circle.radius
print(f"圆的周长: {perimeter:.2f}")
return perimeter
def visit_rectangle(self, rectangle):
perimeter = 2 * (rectangle.width + rectangle.height)
print(f"矩形的周长: {perimeter:.2f}")
return perimeter
使用示例
实例
# 创建图形对象
shapes = [
Circle(5),
Rectangle(4, 6),
Circle(3)
]
# 创建访问者
area_calculator = AreaCalculator()
perimeter_calculator = PerimeterCalculator()
print("=== 计算面积 ===")
for shape in shapes:
shape.accept(area_calculator)
print("\n=== 计算周长 ===")
for shape in shapes:
shape.accept(perimeter_calculator)
shapes = [
Circle(5),
Rectangle(4, 6),
Circle(3)
]
# 创建访问者
area_calculator = AreaCalculator()
perimeter_calculator = PerimeterCalculator()
print("=== 计算面积 ===")
for shape in shapes:
shape.accept(area_calculator)
print("\n=== 计算周长 ===")
for shape in shapes:
shape.accept(perimeter_calculator)
输出结果:
=== 计算面积 === 圆的面积: 78.50 矩形的面积: 24.00 圆的面积: 28.26 === 计算周长 === 圆的周长: 31.40 矩形的周长: 20.00 圆的周长: 18.84
访问者模式的核心组件
1. Visitor(访问者接口)
定义了对每个具体元素类的访问方法。
实例
class ShapeVisitor(ABC):
def visit_circle(self, circle): pass
def visit_rectangle(self, rectangle): pass
def visit_triangle(self, triangle): pass # 可以轻松扩展
def visit_circle(self, circle): pass
def visit_rectangle(self, rectangle): pass
def visit_triangle(self, triangle): pass # 可以轻松扩展
2. ConcreteVisitor(具体访问者)
实现访问者接口中定义的操作。
实例
class ExportVisitor(ShapeVisitor):
def visit_circle(self, circle):
return f'<circle r="{circle.radius}" />'
def visit_rectangle(self, rectangle):
return f'<rect width="{rectangle.width}" height="{rectangle.height}" />'
def visit_circle(self, circle):
return f'<circle r="{circle.radius}" />'
def visit_rectangle(self, rectangle):
return f'<rect width="{rectangle.width}" height="{rectangle.height}" />'
3. Element(元素接口)
定义了接受访问者的方法。
实例
class ShapeElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
@abstractmethod
def accept(self, visitor):
pass
4. ConcreteElement(具体元素)
实现元素接口,在accept方法中调用访问者的对应方法。
实例
class Circle(ShapeElement):
def accept(self, visitor):
visitor.visit_circle(self) # 这里发生了双重分派
def accept(self, visitor):
visitor.visit_circle(self) # 这里发生了双重分派
高级应用示例
复杂的文档处理系统
让我们看一个更实际的例子:处理不同类型的文档元素。
实例
from abc import ABC, abstractmethod
# 文档元素接口
class DocumentElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# 具体文档元素
class Paragraph(DocumentElement):
def __init__(self, text):
self.text = text
def accept(self, visitor):
return visitor.visit_paragraph(self)
class Heading(DocumentElement):
def __init__(self, text, level):
self.text = text
self.level = level
def accept(self, visitor):
return visitor.visit_heading(self)
class List(DocumentElement):
def __init__(self, items):
self.items = items
def accept(self, visitor):
return visitor.visit_list(self)
# 访问者接口
class DocumentVisitor(ABC):
@abstractmethod
def visit_paragraph(self, paragraph): pass
@abstractmethod
def visit_heading(self, heading): pass
@abstractmethod
def visit_list(self, list_element): pass
# 具体访问者:HTML导出
class HTMLExportVisitor(DocumentVisitor):
def visit_paragraph(self, paragraph):
return f"<p>{paragraph.text}</p>"
def visit_heading(self, heading):
return f"<h{heading.level}>{heading.text}</h{heading.level}>"
def visit_list(self, list_element):
items = "".join(f"<li>{item}</li>" for item in list_element.items)
return f"<ul>{items}</ul>"
# 具体访问者:字数统计
class WordCountVisitor(DocumentVisitor):
def __init__(self):
self.total_words = 0
def visit_paragraph(self, paragraph):
words = len(paragraph.text.split())
self.total_words += words
return words
def visit_heading(self, heading):
words = len(heading.text.split())
self.total_words += words
return words
def visit_list(self, list_element):
words = sum(len(item.split()) for item in list_element.items)
self.total_words += words
return words
# 使用示例
document = [
Heading("Python 教程", 1),
Paragraph("Python 是一种强大的编程语言。"),
List(["列表项1", "列表项2", "列表项3"]),
Paragraph("学习Python很有趣。")
]
# HTML导出
html_visitor = HTMLExportVisitor()
print("=== HTML 导出 ===")
for element in document:
print(element.accept(html_visitor))
# 字数统计
word_visitor = WordCountVisitor()
print("\n=== 字数统计 ===")
for element in document:
words = element.accept(word_visitor)
print(f"{element.__class__.__name__}: {words} 个单词")
print(f"总字数: {word_visitor.total_words}")
# 文档元素接口
class DocumentElement(ABC):
@abstractmethod
def accept(self, visitor):
pass
# 具体文档元素
class Paragraph(DocumentElement):
def __init__(self, text):
self.text = text
def accept(self, visitor):
return visitor.visit_paragraph(self)
class Heading(DocumentElement):
def __init__(self, text, level):
self.text = text
self.level = level
def accept(self, visitor):
return visitor.visit_heading(self)
class List(DocumentElement):
def __init__(self, items):
self.items = items
def accept(self, visitor):
return visitor.visit_list(self)
# 访问者接口
class DocumentVisitor(ABC):
@abstractmethod
def visit_paragraph(self, paragraph): pass
@abstractmethod
def visit_heading(self, heading): pass
@abstractmethod
def visit_list(self, list_element): pass
# 具体访问者:HTML导出
class HTMLExportVisitor(DocumentVisitor):
def visit_paragraph(self, paragraph):
return f"<p>{paragraph.text}</p>"
def visit_heading(self, heading):
return f"<h{heading.level}>{heading.text}</h{heading.level}>"
def visit_list(self, list_element):
items = "".join(f"<li>{item}</li>" for item in list_element.items)
return f"<ul>{items}</ul>"
# 具体访问者:字数统计
class WordCountVisitor(DocumentVisitor):
def __init__(self):
self.total_words = 0
def visit_paragraph(self, paragraph):
words = len(paragraph.text.split())
self.total_words += words
return words
def visit_heading(self, heading):
words = len(heading.text.split())
self.total_words += words
return words
def visit_list(self, list_element):
words = sum(len(item.split()) for item in list_element.items)
self.total_words += words
return words
# 使用示例
document = [
Heading("Python 教程", 1),
Paragraph("Python 是一种强大的编程语言。"),
List(["列表项1", "列表项2", "列表项3"]),
Paragraph("学习Python很有趣。")
]
# HTML导出
html_visitor = HTMLExportVisitor()
print("=== HTML 导出 ===")
for element in document:
print(element.accept(html_visitor))
# 字数统计
word_visitor = WordCountVisitor()
print("\n=== 字数统计 ===")
for element in document:
words = element.accept(word_visitor)
print(f"{element.__class__.__name__}: {words} 个单词")
print(f"总字数: {word_visitor.total_words}")
访问者模式的优缺点
优点
- 开闭原则:容易添加新操作,无需修改现有类
- 单一职责原则:将相关行为集中在一个访问者类中
- 灵活性:可以在运行时选择不同的访问者
- 数据分离:算法与数据结构分离
缺点
- 元素接口变更困难:添加新的元素类需要修改所有访问者
- 可能破坏封装:访问者可能需要访问元素的私有成员
- 复杂性:对于简单的数据结构可能显得过于复杂
适用场景
适合使用访问者模式的场景
- 对象结构稳定:需要在一个相对稳定的对象结构上定义多种操作
- 操作经常变化:需要频繁添加新的操作或算法
- 相关操作集中:希望将相关的操作组织在一起
- 复杂对象结构:对象结构包含许多不同类型的对象
实际应用案例
- 编译器:语法树上的各种分析(类型检查、代码优化等)
- 文档处理:对文档元素进行不同的处理(导出、统计、格式化等)
- GUI系统:对UI组件进行不同的操作(渲染、事件处理等)
- 游戏开发:对游戏对象进行不同的处理(碰撞检测、AI等)
最佳实践和注意事项
1. 使用双重分派
访问者模式的核心是双重分派(double dispatch):
- 第一次分派:元素接受访问者
- 第二次分派:访问者访问具体元素
实例
# 在元素中
def accept(self, visitor):
visitor.visit_circle(self) # 这里确定了具体的访问方法
def accept(self, visitor):
visitor.visit_circle(self) # 这里确定了具体的访问方法
2. 处理访问者状态
如果访问者需要维护状态:
实例
class StatisticsVisitor(ShapeVisitor):
def __init__(self):
self.total_area = 0
self.shape_count = 0
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
self.total_area += area
self.shape_count += 1
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
self.total_area += area
self.shape_count += 1
def get_statistics(self):
return {
'total_area': self.total_area,
'shape_count': self.shape_count,
'average_area': self.total_area / self.shape_count if self.shape_count > 0 else 0
}
def __init__(self):
self.total_area = 0
self.shape_count = 0
def visit_circle(self, circle):
area = 3.14 * circle.radius * circle.radius
self.total_area += area
self.shape_count += 1
def visit_rectangle(self, rectangle):
area = rectangle.width * rectangle.height
self.total_area += area
self.shape_count += 1
def get_statistics(self):
return {
'total_area': self.total_area,
'shape_count': self.shape_count,
'average_area': self.total_area / self.shape_count if self.shape_count > 0 else 0
}
3. 处理访问异常
实例
class SafeVisitor(ShapeVisitor):
def visit_circle(self, circle):
try:
# 执行操作
pass
except Exception as e:
print(f"处理圆形时出错: {e}")
def visit_rectangle(self, rectangle):
try:
# 执行操作
pass
except Exception as e:
print(f"处理矩形时出错: {e}")
def visit_circle(self, circle):
try:
# 执行操作
pass
except Exception as e:
print(f"处理圆形时出错: {e}")
def visit_rectangle(self, rectangle):
try:
# 执行操作
pass
except Exception as e:
print(f"处理矩形时出错: {e}")
总结
访问者模式是一个强大的设计模式,它通过将算法与数据结构分离,提供了很好的扩展性。虽然它有一定的复杂性,但在合适的场景下能够显著提高代码的维护性和扩展性。
关键要点:
- 访问者模式适用于对象结构稳定但操作频繁变化的场景
- 通过双重分派机制实现多态行为
- 易于添加新操作,但难以添加新元素类型
- 在实际项目中要权衡其复杂性和带来的好处
点我分享笔记