初始设计模式——访问者模式(Visitor Pattern)

访问者模式(Visitor Pattern)是一种行为型设计模式,其核心思想是将数据结构(元素对象)与对数据的操作(访问逻辑)分离。通过定义独立的访问者类,使得新增操作无需修改元素本身的代码,而是通过扩展访问者类来实现,从而满足 “开闭原则”。访问者模式的本质是将 “数据” 和 “作用于数据的操作” 解耦,使操作集合可独立变化。
访问者模式的组成部分
- 抽象访问者(Visitor):定义针对所有具体元素的访问接口。是所有具体访问者的父类或接口,声明访问行为的规范。
- 具体访问者(ConcreteVisitor):实现抽象访问者定义的访问方法,封装针对特定元素的具体操作逻辑。每个具体访问者可定义不同的操作(如 “计算总价格”“生成报表” 等),专注于特定功能的实现。
- 抽象元素(Element):定义一个accept(Visitor visitor)方法,用于接受访问者的访问。是所有具体元素的父类或接口,声明元素的公共接口(与业务相关的状态或方法)。
- 具体元素(ConcreteElement):实现抽象元素的accept方法,调用访问者对应的访问方法(如visitor.visitConcreteElement(this)),将自身作为参数传递给访问者。具体元素可能包含特有的状态或行为,但通过accept方法将操作委托给访问者处理。
- 对象结构(Object Structure):管理一组元素对象(通常是一个容器,如集合、树结构等),提供遍历或访问元素的接口。可以是具体的数据结构(如列表、树),也可以是元素的聚合类(如Composite模式中的容器节点)。
示例一
在 ThinkPHP5.1 框架中,访问者模式的核心思想被隐性应用于多个组件,但未严格实现模式的所有角色(如抽象访问者、元素的 accept 方法)。比如在查询构建器(Query Builder),操作与数据的分离。
场景
通过链式调用(where()、order()、limit())构建 SQL 查询,最终生成不同数据库的 SQL 语句。
模式映射
- 对象结构:
think\db\Query
类(持有查询条件和表结构)。 - 具体元素:查询条件(如
WHERE id=1
、ORDER BY create_time DESC
)。 - 具体访问者:SQL生成器(如
think\db\connector\Mysql
、think\db\connector\PostgreSQL
)。
核心逻辑
- 条件组装:Query 类通过方法链式调用收集条件(如
where('status', 1)
)。 - SQL转换:调用
fetchSql()
或执行查询时,将条件传递给对应数据库的生成器(如Mysql::parseWhere()
)。 - 操作扩展:新增数据库方言时,只需实现 connector 接口,无需修改 Query 类(符合开闭原则)。
模式体现
- 数据结构(查询条件)与操作(SQL 生成)分离:不同数据库的 SQL 生成逻辑由独立的访问者处理。
- 统一遍历接口:Query 类封装条件遍历,生成器(访问
者)专注于SQL转换。
示例二
场景
在电商平台中,订单包含不同类型商品(实体商品、虚拟商品),需根据商品类型计算税费和折扣。新增计算规则(如运费、积分抵扣)时,不修改商品类本身。
代码
1 |
|
扩展代码
新增运费计算,无需修改原有代码
1 |
|
UML类图
场景价值分析
- 分离数据与操作:商品类(PhysicalProduct)只负责存储属性(名称、价格),计算逻辑(税费、运费)全部交给访问者,符合 MVC 中 “模型专注数据,控制器专注逻辑” 的原则。
- 符合 Web 开发扩展需求:当平台新增 “跨境商品关税” 或 “会员折扣” 时,只需新增DiscountVisitor类,无需修改商品模型或订单类,避免影响已有功能(如支付、库存)。
- 集中复杂逻辑:电商中常见的 “满减计算”“促销叠加” 等复杂规则,可通过访问者类集中管理,避免在控制器中散落大量条件判断(如if($product->type === ‘physical’))。
- 模拟 ThinkPHP 的钩子机制:类似 ThinkPHP 的Hook::listen(‘order_calculate’, $visitor),订单类作为 “对象结构”,通过accept()方法触发注册的访问者(钩子行为),实现扩展解耦。
访问者模式的优缺点
优点
- 分离数据与操作
- 元素类只需关注自身状态,访问逻辑集中在访问者类中,符合 “单一职责原则”。
- 新增操作时,只需扩展访问者类,无需修改元素类或对象结构,符合 “开闭原则”。
- 集中复杂操作
- 对不同元素的相关操作可集中在一个访问者中(如报表生成、语义分析),避免重复代码。
- 支持跨元素操作
- 对象结构中的元素可能属于不同层次或类型,访问者可统一处理这些元素,实现跨类型的全局操作(如遍历树结构并计算所有节点的总权重)。
- 符合依赖倒置原则
- 抽象访问者与抽象元素依赖,具体访问者与具体元素解耦,高层模块(访问者)不依赖底层细节(具体元素的实现)。
缺点
- 元素变更成本高
- 若需要为元素新增属性或方法,所有访问者的访问方法都需要同步修改(因访问者依赖元素的具体接口),违反 “开闭原则”。
- 本质矛盾:访问者模式假设元素结构稳定,而操作易变;若元素频繁变化,模式反而会增加维护成本。
- 违反 “迪米特法则”(最少知识原则)
- 访问者需要了解具体元素的细节(如属性、方法),导致访问者与具体元素紧耦合,尤其是当元素接口复杂时。
- 对象结构与访问者强关联
- 对象结构需要向访问者暴露元素集合(如遍历接口),可能泄露内部表示(如树的节点结构)。
- 双分派机制的复杂性
- 访问者模式依赖 “双分派”(元素调用accept时确定访问者类型,访问者调用具体访问方法时确定元素类型),对新手而言理解成本较高。
访问者模式的适用场景
- 对象结构稳定,操作易变:当元素类型较少变化(如固定的几何图形类Circle、Rectangle),但需要频繁新增操作(如 “计算面积”“渲染图形”“序列化”)时,访问者模式能有效分离操作逻辑。
- 对复杂对象结构进行批量操作:如树形结构(文件系统、AST 抽象语法树)、复合结构(组合模式中的容器与叶子节点),需要对所有节点执行统一或差异化操作(如统计、校验、转换)。
- 跨元素类型的全局操作:当操作需要遍历不同类型的元素并综合处理时(如电商系统中计算订单中不同商品(图书、电子产品)的总税费),访问者可集中处理分支逻辑。
- 需要将数据结构与业务逻辑解耦:例如在编译器中,抽象语法树(元素结构)与语义分析、代码生成(不同访问者)分离,便于扩展新的编译阶段(如优化访问者)而不修改语法树结构。
与其他模式的关联
- 组合模式(Composite):常与访问者模式结合使用,处理树形结构的元素遍历(如组合模式中的容器节点调用子节点的accept方法)。
- 双重分发(Double Dispatch):通过两次动态绑定(元素类型和访问者类型)确定具体执行的方法,是访问者模式的实现基础(在 Java 中通过多态和重载实现)。
- 责任链模式(Chain of Responsibility):两者均处理请求的动态分发,但责任链关注请求的链式传递,而访问者关注数据结构与操作的分离。