设计模式之访问者模式 (Visitor Pattern)

📋 Research Summary

访问者模式是处理复杂对象结构操作的进阶模式。在编译器 AST 遍历、DOM 操作、报表生成中应用。它通过"双分派"机制,将操作与对象结构分离,但代价是增加了系统的复杂性。


🌱 逻辑原点

如果有一个复杂的图形结构(点、线、圆、矩形),你需要支持导出 XML、导出 JSON、计算面积、渲染四种操作,而且未来可能增加更多操作,是为每个图形类添加 4 个方法,还是找到一种方式让操作独立于图形类演化?

操作扩展与类修改的矛盾:操作经常变化,但为每个类添加新方法违反了开闭原则。

🧠 苏格拉底式对话

1️⃣ 现状:最原始的解法是什么?

在每个图形类中添加操作方法:

python 复制代码
class Circle:
    def to_xml(self): ...
    def to_json(self): ...
    def area(self): ...
    def render(self): ...

class Rectangle:
    def to_xml(self): ...
    def to_json(self): ...
    def area(self): ...
    def render(self): ...
# 每个新图形都要实现 4 个方法

优点 :直观,操作与数据在一起。
问题:新增操作需要修改所有图形类,违反开闭原则。

2️⃣ 瓶颈:规模扩大 100 倍时会在哪里崩溃?

当有 50 种图形,需要支持 20 种操作时:

  • 类臃肿:每个图形类有 20 个方法,类变得巨大
  • 违反开闭原则:新增"导出 YAML"操作,50 个类都要修改
  • 职责混乱:图形类既要表示形状,又要处理导出、渲染、计算
  • 无法独立演化:图形结构和操作逻辑耦合,难以分别维护
  • 团队协作冲突:不同开发者修改不同操作,却都要修改同一个图形类

核心矛盾:数据结构相对稳定,但操作经常增加和变化。

3️⃣ 突破:必须引入什么新维度?

将操作封装为独立的访问者类

不是"在每个类中添加方法",而是"创建一个访问者类,它知道如何对所有图形执行某个操作"。图形类只提供一个 accept 方法接收访问者,访问者实现对所有图形的具体操作:

复制代码
图形结构(稳定)
    |
    v
accept(访问者) --双分派--> 访问者.visit具体图形
    |
    v
XML导出访问者 / JSON导出访问者 / 面积计算访问者

这就是访问者的本质:通过双分派,将操作从数据类中分离,让两者独立演化

📊 视觉骨架

接受 accept
访问 visit
访问 visit
访问 visit
实现 implement
实现 implement
实现 implement
元素 Element
访问者 Visitor
具体元素A Concrete A
具体元素B Concrete B
具体元素C Concrete C
XML导出者 XML Exporter
JSON导出者 JSON Exporter
面积计算器 Area Calculator

关键洞察 :访问者模式的核心是"双分派"。element.accept(visitor) 调用后,元素反过来调用 visitor.visit(this),根据元素的实际类型和访问者的实际类型共同决定执行哪个方法。这让新增操作只需要新增访问者类。

⚖️ 权衡模型

公式:

复制代码
Visitor = 解决了操作的独立扩展 + 牺牲了元素类型的扩展性 + 增加了双分派的复杂性

代价分析:

  • 解决: 操作与数据结构解耦、新增操作不影响元素类、相关操作集中在访问者中、可以累积状态(访问者可以保存遍历状态)
  • 牺牲: 新增元素类型困难(需要修改所有访问者)、破坏了元素的封装(访问者需要访问元素内部)
  • ⚠️ 增加: 双分派的理解成本、循环依赖风险(元素依赖访问者接口,访问者依赖具体元素)

使用建议:当对象结构相对稳定,但操作经常增加或变化时使用。典型场景:编译器 AST 操作、报表生成、复杂对象结构的多种导出格式。

🔁 记忆锚点

python 复制代码
class Shape(ABC):
    """
    元素:定义接受访问者的接口
    """
    
    @abstractmethod
    def accept(self, visitor: "Visitor") -> None:
        """双分派的入口"""
        pass

class Circle(Shape):
    """具体元素:将操作委托给访问者"""
    
    def accept(self, visitor: "Visitor") -> None:
        visitor.visit_circle(self)  # 关键:告诉访问者"我是圆形"
    
    def __init__(self, radius: float):
        self.radius = radius

class Visitor(ABC):
    """
    访问者:为每种元素定义操作
    """
    
    @abstractmethod
    def visit_circle(self, circle: Circle) -> None:
        pass
    
    @abstractmethod
    def visit_rectangle(self, rect: "Rectangle") -> None:
        pass

class AreaCalculator(Visitor):
    """具体访问者:计算面积"""
    
    def __init__(self):
        self.total_area = 0
    
    def visit_circle(self, circle: Circle) -> None:
        self.total_area += 3.14159 * circle.radius ** 2
    
    def visit_rectangle(self, rect: "Rectangle") -> None:
        self.total_area += rect.width * rect.height

# 使用:遍历结构,让访问者执行操作
shapes = [Circle(5), Rectangle(4, 5)]
calculator = AreaCalculator()
for shape in shapes:
    shape.accept(calculator)  # 双分派
print(f"Total area: {calculator.total_area}")

一句话本质: 访问者模式 = 通过双分派将操作从数据类中分离,实现操作的独立扩展


相关推荐
sg_knight3 小时前
对象池模式(Object Pool)
python·设计模式·object pool·对象池模式
Yongqiang Cheng3 小时前
设计模式:C++ 单例模式 (Singleton in C++)
设计模式·c++ 单例模式
得一录4 小时前
AI Agent的主流设计模式之反射模式
人工智能·设计模式
我爱cope4 小时前
【从0开始学设计模式-1| 设计模式简介、UML图】
设计模式·uml
※DX3906※4 小时前
Java多线程3--设计模式,线程池,定时器
java·开发语言·ide·设计模式·intellij idea
J_liaty15 小时前
23种设计模式一中介者模式
设计模式·中介者模式
郝学胜-神的一滴1 天前
在Vibe Coding时代,学习设计模式与软件架构
人工智能·学习·设计模式·架构·软件工程
九狼1 天前
Flutter SSE 流式响用 Dio 实现 OpenAI 兼容接口的逐 Token 输出
http·设计模式·api
郝学胜-神的一滴1 天前
单例模式:从经典实现到Vibe Coding时代的思考
开发语言·c++·程序人生·单例模式·设计模式·多线程