
做Unity项目做久了尤其是UI模块一多起来你迟早会撞上一个特别经典的问题十几块面板互相牵扯甲要通知乙乙要通知丙丙又反过来要拉甲入伙。改一处就崩三处代码慢慢变成一团乱麻。我今天要聊的就是23种设计模式里专门治这种“关系混乱”的行为型模式——中介者模式Mediator Pattern。这模式说穿了不玄乎在互相通信的几个对象中间塞一个“总机”。所有对象都只跟总机说话总机再决定把这个消息转给谁。谁都不直接攥着别人的引用耦合自然就降下来了。这篇文章我会先用一个真实项目里的重构案例讲清楚为什么要用它再带你从零撸一套Unity里可直接落地的中介者最后把我踩过的坑和排查技巧全倒出来。适合UI模块复杂、多界面联动的Unity项目开发者尤其是刚接触设计模式、想搞明白中介者到底怎么用的朋友。1. 为什么需要中介者模式从一场UI重构说起1.1 让我决定改代码的“耦合爆炸”现场前两年我接手一个养成类项目里面有个角色养成界面左侧是角色列表右侧是角色详情底下是一排技能栏头顶上还有玩家状态栏。刚开始功能少大家图省事几个Panel之间直接互相拖引用。点击角色列表的项直接在列表里调用详情面板的ShowRole升级之后详情面板调技能面板的UnlockSlot同时调状态栏的RefreshCoin。再加上任务引导系统要监听各种事件就到处理弹窗、飘字的地方都塞了引用。结果第一个需求还好第二个需求也勉强等到加“角色升级后解锁新技能并弹出飘字提示”这个功能时我打开RoleDetailPanel一看升级按钮的点击方法已经长这样了public void OnLevelUpClick() { if (playerStatusPanel.TryPay(_role.LevelUpCost)) { _role.Level; RefreshLevel(); skillPanel.UnlockSlot(skill_slot_2); playerStatusPanel.RefreshCoin(); popupPanel.ShowTip(升级成功); taskSystem.OnRoleLevelUp(_role); // 下一个需求过来还得继续加引用、继续改这里 } }这就是典型的“耦合爆炸”。每个面板手里都攥着其他四五个面板的引用任何一块面板改了方法名所有调它的地方都要跟着改。后面想复用某个面板时发现它拖着整整一个系统根本拖不动。更要命的是这种依赖关系完全隐藏在代码里新来的同事看半天都理不清谁调的谁。我当时痛定思痛决定引入中介者模式。这事的本质不是多写几个类而是把“一对多、多对多”的混乱关系收拢成“多对一、一对多”的清晰关系。1.2 网状通信变成星型通信到底改了什么你想象一下没有中介者时的关系图一堆对象之间画满箭头谁和谁都能连上活像一张蜘蛛网。加中介者之后所有对象只连向中间一个节点网络就变成了一颗星。消息从A发给中介者中介者判断该转发给B还是C或者同时通知B和C。这就是中介者模式的核心思路。这个转变带来的好处很直接。第一是解耦各面板不再需要知道对方的存在角色列表只认识中介者详情面板也只认识中介者。第二是交互逻辑集中谁管谁、谁影响谁这些判断全放在中介者里以后再加需求你打开中介者就能看到完整交互链路而不是翻遍每个Panel找引用。第三是更方便测试和复用每一块面板的内部逻辑和交互逻辑分离了面板可以单独拿出来放到别的场景只要给它配上新的中介者就行。用生活里的例子打比方没有中介者就像几十个同学每人都在通讯录里记着其他所有人的电话任何一个人换号全班都要跟着改备注。中介者就是班群管理员大家有事只找管理员管理员去单独通知或者发群消息。你负责的事情从“记住几十个号码”简化成“知道找谁传话”。1.3 中介者和观察者、事件中心有什么本质区别很多人第一次接触中介者时会把它和观察者模式、Unity里常见的全局事件中心搞混。这里我必须把界限说清楚否则你会在项目里用错方向。观察者模式是典型的广播模型一个主题Subject发生状态变化通知所有订阅它的观察者。发布者完全不关心订阅者是谁它只管喊一句“我变了”爱谁听谁听。Unity里常见的全局EventManager本质上就是观察者模式的一种实现大家向同一个事件总线注册回调。中介者模式则不同。中介者必须知道所有参与者的具体身份甚至知道它们的接口、调用顺序、业务前提。在UI系统里中介者往往直接持有各面板引用主动调用它们的方法。这不是广播而是由中心节点“点名”安排任务。做个简单对比对比项观察者模式中介者模式中心对象是否知道参与者不知道只管广播知道并且会主动调度消息方向一对多、单向推送多对多、由中心路由典型场景生命周期广播、全局事件UI模块联动、复杂交互编排Unity实现事件总线、C# event场景里的Mediator节点耦合程度发布者与订阅者解耦同事间解耦但都与中心耦合这里还要顺带提一句外观模式Facade。外观模式是给一个复杂的子系统提供一个统一入口调用方向是单向的外部调用外观外观调用内部。中介者则不同参与者之间的交互是双向的A发消息给中介者中介者转给BB也可能再发消息给中介者中介者再转给A。把这个区别记住面试和做架构都不容易吃亏。2. 中介者模式的经典结构与工程取舍2.1 拆开经典四件套Mediator、Colleague都在干什么在GoF的23种设计模式里中介者模式被归为行为型模式。一套经典结构包含四类角色抽象中介者Mediator、具体中介者ConcreteMediator、抽象同事Colleague、具体同事ConcreteColleague。抽象中介者定义了同事之间通信的接口一般就是一到两个方法比如Notify(sender, eventType, args)。具体中介者实现这个接口内部持有所有需要协调的同事引用在收到消息后决定调用哪些同事的哪些方法。抽象同事通常是个基类里面保存一个中介者的引用并提供注册中介者入口。具体同事实现自己真正要干的业务逻辑当需要和其他同事协作时就调用中介者的Notify方法。拿Unity里的场景举例会非常直观。假设一套养成界面抽象同事Colleague是一个MonoBehaviour基类里面有一个叫SetMediator的方法具体同事包括RoleListPanel、RoleDetailPanel、PlayerStatusPanel、SkillPanel。抽象中介者就是一个接口比如IUIMediator具体中介者是挂在场景空节点上的UIMediator它知道所有面板引用并且拥有完整的交互规则。这套结构里有一个非常关键的设计点具体同事之间不建立任何引用关系。RoleListPanel点击一个角色后做的事情只有一行调中介者的Notify。真正去刷新详情面板、检查技能解锁、更新状态栏的逻辑全部放在中介者里。因而每一块Panel都可以独立修改而不影响其他Panel。2.2 实战里的第一刀我建议砍掉抽象中介者教科书上通常会让你先定义IMediator接口再实现UIMediator类。但在我实际做Unity项目的经验里这个抽象层很多时候可以砍掉。如果整个项目只有一套中介者或者你没有替换实现、写单元测试的硬需求那直接写一个具体的UIMediator类让Colleague持有它完全够用。为什么敢砍因为抽象的价值在于多态而现在没有多态场景。写一个接口里面就一个Notify全局只有一个实现类这种接口除了增加一层文件跳转之外其收益微乎其微。我曾经在一个项目里为了“规范”保留了IMediator结果后来真的去搜谁引用了这个接口发现全世界只有一个实现。后来做代码清理时直接把它删掉所有Colleague类里的字段类型改为UIMediator编译通过逻辑一点没变。当然有两种情况值得保留抽象。第一系统里有明显多于一套中介者比如战斗中介者、主城中介者、副本中介者且它们之间的行为差异很大那么抽象出IMediator可以让上层代码统一调度。第二你有测试需求想在纯C#环境下用Mock中介者去验证同事类的发送逻辑。没有这两个前提别为抽象而抽象。2.3 消息签名为什么用枚举和object而不是字符串中介者对外暴露的通信方法一般长这样Notify(Colleague sender, 消息类型, object data)。设计消息类型时有三种常见做法用字符串、用强类型方法、用枚举。字符串是最容易写的但也是坑最多的。拼写错误编译期发现不了改名时IDE重构不识别消息多了以后你根本记不清是“RoleSelected”还是“RoleSelect”还是“role_selected”。团队里只要有一次拼错线上就会出现一个莫名其妙的“消息丢失”问题。强类型方法则意味着中介者对外暴露多个方法比如OnRoleSelected(RoleData role)、OnLevelUpRequest(RoleData role)。每个方法都是一份文档编译期也安全。但它的代价是同事类需要知道中介者有哪些方法而在许多动态交互场景里消息类型会持续演进每加一种交互就得在中介者上加一个方法并重新编译灵活性差一些。我的习惯是枚举加object。枚举有IDE补全有类型检查还能在switch里让编译器提示未覆盖的分支。object是万能箱子任何时候想加一种新消息只加一个枚举值不需要动方法签名。代价是要代价转换——取出data时先判空再转型。对于内部模块通信来说这个代价完全可以接受。如果你觉得object太野可以定义一个MediatorMessage类把sender、msgType、data、发送时间都包起来后面排查问题更好使。3. Unity实操跟着我从零搭一套养成界面中介者3.1 场景设定三个面板的联动需求为了让你真能照着写我设计一个足够简单、但能覆盖中介者典型用法的场景。一个养成界面包含以下面板RoleListPanel左侧角色列表点击任意角色列表项右侧需要显示对应角色详情。RoleDetailPanel右侧角色详情展示角色等级、属性、升级按钮。点击升级按钮后如果金币足够角色升级、扣金币、刷新显示。PlayerStatusPanel顶部状态栏显示玩家金币。角色升级后金币要同步刷新。SkillPanel底部技能面板。角色升到3级后解锁第二个技能槽位并播放解锁特效。PopupPanel通用飘字提示升级成功、金币不足等反馈都通过它弹。需求有限但已经形成了一条需要多人转发的链路角色列表 → 详情 → 升级判断 → 状态栏 技能面板 飘字提示。如果直接用引用互相调又是当初那团乱麻。用中介者正好。3.2 完整代码实现一步步说清楚每个类的职责先定义消息类型枚举public enum UIMessageType { RoleSelected, LevelUpRequest, CoinChanged, SkillUnlocked }再看数据模型。为了演示用最简单的RoleData类[System.Serializable] public class RoleData { public string roleName; public int level; public int levelUpCost; public void LevelUp() { level; levelUpCost 50; } }然后是抽象同事基类。这里我只保留了中介者引用和注册入口没有把消息发送方法封装进去因为不同同事需要发送的消息长得不一样封装一个通用方法反而多余public abstract class Colleague : MonoBehaviour { [HideInInspector] public UIMediator Mediator { get; private set; } public void SetMediator(UIMediator mediator) { Mediator mediator; } }接着是核心的UIMediator它挂在场景里的一个空节点上。四个面板引用直接拖到Inspector里using UnityEngine; public class UIMediator : MonoBehaviour { [SerializeField] private RoleListPanel roleListPanel; [SerializeField] private RoleDetailPanel roleDetailPanel; [SerializeField] private PlayerStatusPanel playerStatusPanel; [SerializeField] private SkillPanel skillPanel; [SerializeField] private PopupPanel popupPanel; private void Start() { roleListPanel.SetMediator(this); roleDetailPanel.SetMediator(this); playerStatusPanel.SetMediator(this); skillPanel.SetMediator(this); popupPanel.SetMediator(this); } public void Notify(Colleague sender, UIMessageType msgType, object data) { switch (msgType) { case UIMessageType.RoleSelected: var role data as RoleData; if (role ! null) { roleDetailPanel.ShowRole(role); } break; case UIMessageType.LevelUpRequest: var target data as RoleData; if (target ! null) { HandleLevelUp(target); } break; } } private void HandleLevelUp(RoleData role) { if (playerStatusPanel.TryPay(role.levelUpCost) false) { popupPanel.ShowTip(金币不足); return; } role.LevelUp(); roleDetailPanel.RefreshLevel(role.level); playerStatusPanel.RefreshCoin(); popupPanel.ShowTip(role.roleName 升级成功); if (role.level 3) { skillPanel.UnlockSlot(skill_slot_2); Notify(this, UIMessageType.SkillUnlocked, skill_slot_2); } } }这里的关键点是中介者直接持有所有面板引用它知道由谁发起消息、该通知谁、需要做什么判断。RoleDetailPanel不需要知道PlayerStatusPanel存在也不需要知道技能面板存在。它只负责把“用户点了升级按钮”这个请求发出去剩下的判断和连锁反应都是中介者的事。再看几个同事类的写法。RoleListPanelpublic class RoleListPanel : Colleague { public void OnRoleItemClick(RoleData role) { Mediator.Notify(this, UIMessageType.RoleSelected, role); } }RoleDetailPanelpublic class RoleDetailPanel : Colleague { private RoleData _currentRole; public void ShowRole(RoleData role) { _currentRole role; // 刷新头像、名字、等级、属性 } public void RefreshLevel(int level) { // 只更新等级显示文本 } public void OnLevelUpButtonClick() { if (_currentRole ! null) { Mediator.Notify(this, UIMessageType.LevelUpRequest, _currentRole); } } }SkillPanel、PlayerStatusPanel、PopupPanel的写法也都类似需要别人帮忙时用Mediator.Notify发消息被别人调用时只负责执行自身展示逻辑。用这套结构再回头看那个Upgrade按钮RoleDetailPanel完全不知道金币、技能、飘字、任务系统它的职责只有两个展示角色数据和报告用户操作。所有再把“升级后要发任务事件”加到HandleLevelUp里时只需要改中介者其他面板零改动。这就是中介者模式在你项目里最实在的收益。3.3 易用的细节消息日志、空安全与分发队列上面这套代码可以跑但直接用进正式项目前我建议加三个增强细节。第一个是消息日志。中介者模式把交互集中了按理说排查问题会更方便但前提是你知道“哪条消息从哪来、发给谁”。我在UIMediator里加了一个debug开关开发模式下打开每次Notify都打一条结构化日志包括发送者名字、消息类型、数据内容这样你在Console窗口里可以按时间线完整复盘一次交互链路[SerializeField] private bool debugMode; public void Notify(Colleague sender, UIMessageType msgType, object data) { if (debugMode) { Debug.Log($[UIMediator] {sender?.name} - {msgType} - {data}, this); } // 原有 switch 逻辑 }第二个是空安全。同事面板可能在场景还没初始化完时就发消息或者某种情况下被销毁了。中介者的Notity里所有从data转换出来的对象都要判空调用同事方法前也要考虑对方是否null。如果你偷懒不判空一个隐藏的空引用会让你多排查好几个小时。第三个是分发队列。我遇到过一个比较隐蔽的问题中介者在处理A消息时内部又发了一条B消息B消息又触发A消息同一帧内递归越来越深最后栈溢出。解决方式是把消息分发改成“先入队再逐条处理”。框架不大时不需要上消息队列但至少要有这个意识在HandleLevelUp这类方法里尽量减少额外的Notify嵌套。当交互复杂到一定程度用一个List存待处理消息在Update里循环派发会更稳。4. 中介者模式的避坑指南与问题排查4.1 别把它做成上帝对象中介者模式最大的争议点在于它把复杂度从多个类搬到了一个类里。如果控制不住UIMediator会变成几千行。它什么都管谁都认识最后成了另一种“上帝对象”跟当初的耦合爆炸相比只是换了一种死法。我踩过的坑是一开始把所有交互规则都写进中介者包括复杂的数值计算、任务激活条件、红点刷新规则。结果中介者越来越大每次改需求都要动它其他人也觉得“反正都往中介者里塞就行”。后来项目里中介者几乎成了业务逻辑中心比Panel还臃肿。破解办法就三条。第一中介者只做路由和编排不写业务规则。升级要扣多少钱、任务要不要解锁这些逻辑放到数据层或者专门的Manager里中介者只负责调用。第二把大的中介者拆成子模块。比如一个主界面中介者里面不要全部塞满可以让每个Tab页拥有一个TabMediator主中介者只负责协调各TabMediator之间的交互。第三记住一个判断标准当你发现某个方法里的switch分支超过六七个并且每个分支都在做不同的业务处理是时候考虑拆了。中介者要的是一个清晰的“转发表”不是一锅炖。4.2 调试时怎么快速定位“消息去哪了”中介者模式里最常见的调试痛点就是消息链路不透明。A发了消息中介者好像没处理或者处理了但没通知到想要的同事。我的经验是三层排查法。第一层看发送端。在同事类的发消息方法旁打断点确认消息类型和数据真的传出去了。很多时候问题不是中介者逻辑错而是同事类在某个状态下压根没有调用Notify或者传了一个null的data。第二层看中介者入口。在Notify方法开头打日志或断点确认消息已经到达中心。这一步能区分问题出现在发送还是分发。我打开日志后经常发现消息根本没到中介者原因就是发送方的Mediator字段没被SetMediator赋值场景里拖引用时漏了。第三层看接收端。确认中介者已经调用了目标面板的方法但面板没有反应。这时候问题就落在具体同事类的实现里比如UI元素没有刷新、用了缓存数据。这时候别再倒回中介者直接查面板自己的逻辑就行。还有一个系统性的技巧给MediatorMessage加上一个自增序号把日志打印出来后按序号排一次序。高频率消息下你一眼就能看出哪些消息被延迟、哪些递归触发、哪些在同一帧重复发送。这个方法在连续点击和快速切换角色这类场景里特别好用。4.3 哪些场景其实不该用中介者写中介者模式的文章大多在讲它的好处但作为实际做项目的人我必须给你说清楚它的边界。如果你只有两个或者三个对象互调直接用引用或者一个UnityEvent就完了硬套中介者只会增加一层毫无意义的间接层代码读起来更绕。如果交互是典型的广播场景比如玩家死亡、敌人刷新、时间缩放变化这类事件特征是“我不关心谁在听你们自己决定要不要处理”那用观察者模式更自然。中介者适合的是“必须由中心统筹安排”的场景参与各方都需要被中心点名去完成具体动作顺序还有讲究。UI各面板之间的联动、玩法系统和界面的协调、需要按顺序执行多个步骤的复杂交互这才是中介者模式的主场。另外还要考虑团队的心智负担。中介者模式的维护成本很大程度取决于团队里每个人是否理解“所有通信都走中心”这条约定。如果某位同事图省事绕过中介者直接引用其他Panel这套架构就崩了。所以引入前先统一约定并且在代码审查里重点关注。4.4 结合Unity生命周期的几个性能与内存注意点跟Unity引擎打交道不能只盯着设计模式还得考虑生命周期和性能。中介者模式在Unity里有几个容易踩的坑我单独挑出来说。生命周期问题同事类如果销毁了而中介者还持有它的引用再次调用就会报MissingReferenceException这类异常特别难排查因为引用看起来还在但底层对象已经没了。我的建议是凡是可动态创建销毁的面板在中介者里要提供Register和Unregister方法同事的OnDestroy主动注销自己。中介者内部最好用Dictionary存储同事引用销毁时移除别用数组硬存。性能问题中介者的switch分发在消息频率不高时完全没问题但如果你每秒触发几百次UI事件每次还要做对象转型和分支判断多少会产生一些开销。高频场景可以改成DictionaryUIMessageType, List 的委托分发表注册一次查询直接调用省掉一串if-else。不过就大多数UI项目的频率来说switch绰绰有余不要为了性能提前优化。还有一个容易被忽略的坑中介者本身不要做成全局单例。把它挂在场景的节点上作为场景局部的中枢生命周期跟随场景走。单例中介者会让多个界面共用一份状态一旦场景切换没清理干净消息就会扰到另一个场景里。真需要跨场景通信时回到观察者模式的事件总线而不是扩中介者的管辖范围。5. 再进一步中介者模式在Unity里还能怎么玩5.1 用中介者实现轻量级MVC中介者模式很适合和MVC思路结合尤其是Unity UI这块。你把视图层理解为各类Panel模型层理解为角色数据、玩家数据控制层就是中介者。中介者既监听视图发来的用户操作又根据模型状态去刷新视图天然就是Controller的角色。我做一个活动页面时就用了这套思路。活动面板只负责收集用户点击活动项、兑换按钮等操作然后发给ActivityMediator。ActivityMediator再查询活动数据的Model层判断是否满足兑换条件满足就调用数据层扣积分、发奖励最后刷新活动面板、弹窗、红点。视图之间零引用数据层不依赖任何视图中介者负责对接。这样后面把界面UI换一套数据层完全不用动把中介者里的视图引用替换一下就行。5.2 多中介者协调的模块化设计一个大型项目可能同时存在好几套中介者战斗界面一套、主城界面一套、背包界面一套。每套中介者服务自己的模块彼此之间尽量不要直接通信。确实需要跨模块时应该由上一层的总控来协调或者通过事件总线广播一个“领域事件”让其他模块各自决定要不要响应。这里要特别提醒一点不要把所有中介者都塞进一个类里管理。中介者模式的价值在于让每个模块的内部关系变得清晰如果你在主城中介者里又处理背包面板和角色面板的关系那主城中介者又会涨成“上帝对象”。我在实战里更喜欢每个模块独立挂一个Mediator节点模块之间通过事件解耦这样每个Mediator的职责都是小而明确的。毕竟模式是死的架构是活的把它用在我们项目自己的痛点上比背下一百种UML图都管用。