访问者模式是一种行为型设计模式, 核心思想是: 分离对象的数据结构(”被访问者”)和对数据的操作(”访问者”) 让操作可以独立扩展, 而不五修改被访问者代码
简单来说: 被访问者(比如游戏中的角色\道具)只负责”存在”, 访问者(比如道具效果\伤害计算)负责”对它做什么”. 当需要新增操作时, 只需要加新的访问者, 不用改变被访问者的代码.
生活中的例子: 快递分拣
快递站里有各种包裹(被访问者: 文件\电器\生鲜), 分拣员(访问者: 普通分拣员\冷链分拣员) 负责对不同包裹做不同处理:
- 普通分拣员: 文件 → 常温区, 电器 → 易碎区, 生鲜 → 暂存区;
- 冷链分拣员: 文件 → 常温去, 电器 → 易碎区, 生鲜 → 冷链区;
包裹(被访问者)的结构不变 (都是”要被分拣的东西”), 但分拣规则(访问者的操作)可独立扩展(新增”加急分拣员”只需加新访问者, 不用改包裹)
Unity中的核心场景: 道具效果系统
假设我们开发一款RPG游戏, 有3种角色(玩家\友方NPC\地方怪物)和3种道具(生命药水\攻击Buff\陷阱). 每种道具对不同角色的效果完全不同
道具\角色 | 玩家(Player) | 友方NPC(Ally) | 敌方怪物(Enemy) |
生命药水 | 恢复 100 血 | 恢复 80 血 | 扣 50 血(毒药水) |
攻击Buff | 攻击 + 20(持续5秒) | 攻击 + 15(持续3秒) | 无效(免疫buff) |
陷阱 | 扣除 30 血 + 短暂硬直 | 扣除 20 血 | 扣除 60 血 + 2秒硬直 |
不用访问者模式的问题:
如果直接在角色脚本里写
if-else 判断道具类型:- 耦合度极高: 角色和道具效果强绑定;
- 扩展困难: 新增道具(如防御Buff), 需要修改所有角色的代码;
- 维护复杂: 道具效果逻辑分散在各个角色种, 不好统一管理.
用访问者模式重构: 核心角色
访问者模式有4个核心角色, 对应Unity场景如下:
模式角色 | Unity场景映射 | 作用 |
抽象访问者( IVisitor) | IItemVisitor(道具访问者) | 定义访问者所有”被访问者”(角色)的方法 |
具体访问者( ConcreteVisitor) | HealthPotionVisitor\AttackBuffVisitor等 | 实现道具对不同角色的具体效果 |
抽象元素( IElement) | ICharacter(角色接口) | 定义”接受访问者”的方法( Accept) |
具体元素( ConcreteElement) | Player\AllyNPC\Enemy | 实现 Accept方法, 让访问者”访问”自己 |
对象结构( ObjectStructure) | CharacterManager(角色管理器) | 管理所有角色, 提供遍历接口(可选) |
- 定义抽象元素(
ICharacter)
所有角色(被访问者)都要实现这个接口, 核心是
Accept 方法 ——— 让访问者能”访问”自己, 并调用访问者对应的处理逻辑.- 定义抽象访问者(
IItemVisitor)
所有道具(访问者)都要实现这个接口, 定义对每种角色的处理方法(
VisitPlayer\VisitAlly\VisitEnemy).- 实现具体元素(角色:
Player\AllyNPC\Enemy)
每个角色实现
Accept 方法, 核心逻辑是: 调用访问者对应的”访问自己”的方法, 并把自己传进去.角色本身只负责”持有自身数据”(血量\攻击力), 不关心道具效果(交给访问者处理).
- 实现具体访问者(道具:
HealthPotion\AttackBuff\Trap)
每个道具(访问者)实现
IItemVisitor, 定义对每种角色的具体效果 ——— 所有道具效果逻辑都集中在这里, 和角色解耦.- 实现对象结构(
CharacterManager)
管理所有角色, 提供”遍历所有角色并应用道具效果”的接口(可选, 但Unity种常用管理器简化调用)
Unity中实际使用
- 创建角色对象:
- 创建 3 个游戏对象(Player、AllyNPC、Enemy);
- 分别挂载
Player、AllyNPC、Enemy脚本; - 在
Start方法中调用CharacterManager.Instance.AddCharacter(this),将角色加入管理器。
- 创建道具按钮:
- 在 UI 中创建 3 个按钮(生命药水、攻击 Buff、陷阱);
- 给每个按钮绑定
OnClick事件,调用CharacterManager.Instance.ApplyItemEffect(新的访问者实例): - 生命药水按钮:
CharacterManager.Instance.ApplyItemEffect(new HealthPotionVisitor()); - 攻击 Buff 按钮:
CharacterManager.Instance.ApplyItemEffect(new AttackBuffVisitor()); - 陷阱按钮:
CharacterManager.Instance.ApplyItemEffect(new TrapVisitor())。
- 运行测试:
- 点击不同按钮,控制台会输出每种道具对不同角色的效果;
- 新增道具(如防御 Buff)时,只需新增
DefenseBuffVisitor类(实现IItemVisitor),无需修改任何角色脚本!
访问者模式的核心优点
- 解耦数据与操作: 角色(数据结构)和道具效果(操作)完全分离, 角色不用关心”道具对我做什么”, 道具不关心”角色用什么数据”(通过角色的公开方法操作).
- 扩展灵活: 新增道具只需加新的
IItemVisitor实现, 不同改所有角色代码(符合开放/封比原则).
- 逻辑集中: 所有道具效果集中在访问者类中, 便于维护和修改(比如要调整生命药水的恢复量,只需要改
HealthPotionVisitor).
- 支持多态操作: 同一道具对不同角色有不同效果, 通过访问者的多态方法自动匹配.
访问者模式的适用场景(Unity中)
- 道具/技能效果系统: 如本例, 同一技能/道具对不同角色(玩家\敌人\NPC)有不同效果
- 伤害计算系统: 不同伤害类型(物理\魔法\真实伤害)对不同防御类型(护甲\魔抗\无防御)的计算逻辑.
- UI数据显示: 同一组数据(角色属性)需要在不同UI面板(角色面板\战斗面板\排行榜)中展示不同格式, 用访问者处理展示逻辑.
- 资源检查工具: 在编译器扩展中, 遍历场景中的所有
GameObject(被访问者), 用访问者检查是否有缺失的组件\无效的引用.
- 敌人弱点打击: 不同武器(访问者)对敌人不同部位(被访问者: 头部\身体\四肢)早场不同伤害(如弓箭打头部暴击, 剑打身体普通伤害).
访问者模式的注意事项
- 元素(被访问者)稳定时更适用: 如果需要频繁更新角色类型(如新增”中立单位”), 则选哟修改
IItemVisitor接口(加VisitNeutreal方法)混合所有访问者实现, 成本较高.
- 依赖被访问者的公开接口: 访问者只能通过被访问者的公开方法操作数据, 不能直接访问私有字段(保证数据封装).
- 避免过渡使用: 如操作很少变化(比如道具效果固定), 则无需使用访问者模式, 直接在角色中处理更简单.
总结
访问者模式的核心是”谁访问谁, 做什么” ——— 被访问者(角色)提供”被访问”的入口(Accept), 访问者(道具)提供”访问后的操作”(
VisitXXX). 在Unity中, 它特别适合”数据结构稳定\操作多变”的场景(如道具\技能\伤害系统), 能极大提升代码的可扩展性和可维护性.比如后续要做”元素伤害系统”(火焰\冰霜\雷电伤害), 只需新增
FireDamageVisitor\IceDamageVisitor, 让敌人\玩家接受这些访问者的处理,完全不用动原有角色代码!问题
- 用Unity形式讲解访问者模式时, 如何设计抽象元素和具体元素?
- 用Unity形式讲解访问模式时, 如何设计对象结构?
- 用Unity形式讲解访问模式时, 如何在场景中应用访问模式?