设计模式之访问者模式 (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}")

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


相关推荐
海特伟业32 分钟前
隧道调频广播覆盖-隧道调频广播无线覆盖系统建设要点、难点分析与解决应对
运维·设计模式
sg_knight39 分钟前
设计模式实战:享元模式(Flyweight)
python·设计模式·享元模式·flyweight
Swift社区3 小时前
AI 时代,ArkUI 的设计模式会改变吗?
人工智能·设计模式
数据中穿行3 小时前
访问者设计模式全方位深度解析
设计模式
宁雨桥4 小时前
前端设计模式面试题大全
前端·设计模式
数据中穿行5 小时前
迭代器设计模式全方位深度解析
设计模式
数据中穿行5 小时前
观察者设计模式全方位深度解析
设计模式
程序员Terry6 小时前
别老写重复代码了!模版方法模式一次讲透
java·设计模式
数据中穿行6 小时前
建造者模式全方位深度解析
设计模式
数据中穿行6 小时前
组合设计模式全方位深度解析
设计模式