资讯详情

资讯详情

中介模式实战指南:从多对多耦合到星型协作的解耦之道

1. 中介模式的前世今生从一锅粥到人人清爽先说个最直观的场景。你办公室里十个人都在同一个项目组今天张三找李四确认接口明天王五问赵六要测试环境后天钱七找孙八对排期。每个人都得记着别人的联系方式、工作节奏、脾气秉性。一旦有人离职或者转岗整个信息网就得重新梳理一遍沟通成本高得吓人。软件系统里的事情一模一样。假设你负责一个复杂的订单流程用户下单后要同时通知库存系统扣减、支付系统发起收款、物流系统生成运单、营销系统发放积分、消息系统推送通知。如果每个模块之间直接互相调用代码里就会织出一张密密麻麻的蜘蛛网——库存模块要知道支付模块怎么调支付模块要知道物流模块的生单接口叫什么物流模块还要反过来通知营销模块。改一个接口后面跟着一串模块都要跟着动测试的时候牵一发而动全身上线的时候提心吊胆生怕漏改了哪条调用关系。这就是典型的过度耦合也是Mediator中介模式要解决的问题。中介模式的核心思想说穿了就是有事找中间人别直接找对方。就像合租的时候有一个房东统一管水电网费租客之间不需要互相扯皮所有问题都找房东一个人解决。换成技术术语来说就是让各个模块之间的多对多关系简化为每个模块只和中介者进行一对一的交互。我最早接触这个模式是在几年前做一个企业内部的管理系统当时前端页面上有表格、筛选器、分页器、详情弹窗、面包屑导航这几个组件之间的联动关系多到爆炸。表格要监听筛选器变化重新加载数据分页器要同步当前页码详情弹窗要接收表格选中的行数据面包屑要跟着详情弹窗开关变化。一开始我直接在组件之间互相引用写了不到一百个方法调用代码就乱成一团。后来用中介模式重构成一个事件总线式的协调器所有组件只跟协调器打交道整个联调过程顺了不止一点半点。这个模式属于GoF经典设计模式中的行为型模式适用场景非常明确当对象之间呈网状交互、彼此依赖纠缠不清的时候通过增加一个中介对象把网状结构变成星型结构。它的底层逻辑不复杂但用好它需要的不是背概念而是真正理解什么情况下该用、什么情况下不该用、用了之后怎么避免把中介者写成上帝对象。这部分实操经验网上讲得多的不多我今天一并说清楚。适合阅读这篇内容的人我建议是已经写过一段时间业务代码、看过几个设计模式、开始有意识关注代码可维护性的开发者。如果你还在学语法阶段这篇也能看懂但更建议先动手写几百行代码再回头读感受会深很多。2. 中介模式为什么能治多对多耦合这个病2.1 没有中介者的世界盘根错节的调用关系要想理解中介模式的价值得先看清楚它要解决的问题到底有多痛。举个例子你有一个在线教育平台的系统核心业务是学生报名课程后触发的整个流程。不引入中介者之前代码大概长这样// 课程报名模块直接调用一堆其他模块 class CourseEnrollmentService { enroll(student: Student, course: Course) { // 检查库存 this.inventoryService.reduceStock(course.id); // 创建支付订单 this.paymentService.createOrder(student.id, course.price); // 发送通知 this.notificationService.sendEmail(student.email, 报名成功); // 发放优惠券 this.couponService.issueNewUserCoupon(student.id); } }这只是报名一个动作。如果后续还有退课、换班、转赠、补差价这些操作每个操作都要重复调用上面那一堆服务。一旦某一天通知服务从发邮件改成发短信你要去所有调用过通知的地方改一遍。更麻烦的是新增一个模块时比如用户报名后要给他开一个学习社群权限你就得去所有涉及报名的地方把社群服务加进去漏一处就是一个线上事故。这种设计下的代码单看每个服务本身都很干净但整体组合起来就是一团乱麻。用专业点的话说这叫显式的多对多引用对象之间互相知道对方的存在任何一个对象的变化都会引发连锁反应。程序员的维护成本从读懂一个类的逻辑上升为理解所有类之间的调用路径认知负担成倍增长。2.2 引入中介者之后星型结构的威力中介模式的做法是引入一个协调中心所有模块之间的通信都通过这个协调中心转发。还用上面报名的例子我们把协调逻辑抽到一个组件内部// 中介者统一协调整个报名流程 class EnrollmentMediator { private inventory: InventoryService; private payment: PaymentService; private notification: NotificationService; private coupon: CouponService; private community: CommunityService; handleEnrollment(student: Student, course: Course) { this.inventory.reduceStock(course.id); this.payment.createOrder(student.id, course.price); this.notification.sendEmail(student.email, 报名成功); this.coupon.issueNewUserCoupon(student.id); this.community.grantAccess(student.id, course.id); } }现在报名模块只需要做一件事把某个学生报名了某课程这个事实告诉中介者剩下所有连锁反应全部交给中介者内部去编排。报名模块不再关心通知怎么发、库存怎么扣、社群权限怎么开它就只管自己最核心的职责——记录一个报名事件。改动以后的调用关系变成了标准的星型所有模块指向中介者中介者统一协调。课程报名模块和库存、支付、通知等模块之间不再有任何直接引用关系。新增一个环节比如报名后赠送VIP会员改动范围从所有涉及报名的调用点收缩为只改中介者一个类。你觉得哪个维护起来更省心一眼就能看出来。这种设计本质上在做一件事把流程编排和业务执行解耦。业务执行是各个模块自己的事流程编排是中介者的事。前者只需要知道我该干什么后者需要知道整体流程该怎么走。把两种关注点分开是中介模式最核心的思维。2.3 生活类比为什么群租房要有个房东再换一个生活化的视角来理解。几个年轻人合租如果水电气费各自和不同的公司打交道网费跟运营商谈房租跟房东谈维修打物业电话垃圾清运喊社区那每天都有一堆沟通成本。但如果你请一个人统一当管家租客只管把需求告诉他由他去对接水电公司、物业、网络运营商整个事情就简化了。不过这里面有个重点值得注意管家不是传声筒他要对整体负责。正常的中介者不是简单地转发消息而是要理解流程、控制顺序、甚至能做决策。比如报名时库存不足中介者可以直接拒绝这次报名而不是傻乎乎地把库存扣减报错透传给上层。这是很多人用中介模式容易忽略的地方我后面实操环节再强调。3. 手写一个中介模式从需求分析到实现落地3.1 需求场景一个页面里的复杂组件联动接下来用一个非常贴近日常开发的需求来完整实践一遍中介模式。假设你要做一个带数据管理后台风格的页面页面上有四个核心组件搜索栏用户输入关键词后触发搜索表格区展示搜索结果支持行选中展示当前数据状态分页器显示当前页码、总页数支持翻页和修改每页条数状态提示区显示正在加载共xx条数据已选中xx行等状态信息不引入中介模式的话搜索栏搜完要通知表格重新请求、通知分页器重置页码、通知状态区显示结果条数。表格选中一行要通知状态区、通知详情按钮是否可用。分页器翻页要通知表格重新请求、通知状态区页码变化。这几个组件之间的依赖如下搜索栏 - 表格搜索栏 - 分页器搜索栏 - 状态区表格 - 状态区表格 - 详情激活区分页器 - 表格分页器 - 状态区每个组件都持有其他组件的实例任何一个触发逻辑变化所有相关方都要跟着调整。看着还不算太夸张但组件数量一旦增加到七个、八个调用矩阵就会爆炸。3.2 需要一个中介者来承担联动逻辑按中介模式来设计我们让一个页面协调器PageMediator持有所有组件的引用组件之间不直接通信全部通过协调器转发我把完整的代码实现放在下面使用的是TypeScript方便看类型关系interface SearchBar { setKeyword(keyword: string): void; getKeyword(): string; } interface Table { setData(data: any[]): void; setSelectedRow(row: any | null): void; } interface Pagination { setCurrentPage(page: number): void; setTotal(total: number): void; } interface StatusBar { showLoading(): void; showResult(total: number, keyword: string): void; showSelection(row: any | null): void; } class PageMediator { constructor( private searchBar: SearchBar, private table: Table, private pagination: Pagination, private statusBar: StatusBar ) {} onSearchKeywordChanged(keyword: string) { this.statusBar.showLoading(); this.pagination.setCurrentPage(1); console.log(发起搜索请求关键字${keyword}); // 实际项目中这里会调用某个API服务 setTimeout(() { const mockResult: any[] [ { id: 1, name: ${keyword}相关的记录A }, { id: 2, name: ${keyword}相关的记录B }, { id: 3, name: ${keyword}相关的记录C }, ]; this.table.setData(mockResult); this.pagination.setTotal(mockResult.length); this.statusBar.showResult(mockResult.length, keyword); }, 200); } onPageChanged(page: number) { console.log(切换到第${page}页并请求数据); this.statusBar.showLoading(); // 重新请求数据后通知表格和状态区 this.table.setData([]); this.statusBar.showResult(0, ); } onRowSelected(row: any | null) { this.statusBar.showSelection(row); } } // 各组件只需要知道我有一个方法可以把数据状态传给外界 class ConcreteTable implements Table { private data: any[] []; private mediator: PageMediator; setMediator(mediator: PageMediator) { this.mediator mediator; } setData(data: any[]) { this.data data; } setSelectedRow(row: any | null) { this.mediator.onRowSelected(row); } }看这个实现 组件之间没有直接互相引用它们都指向同一个中介对象。搜索栏被输入时调mediator.onSearchKeywordChanged分页器改变时调mediator.onPageChanged表格选中行时调mediator.onRowSelected。中介者内部统一编排动作保证流程是完整的、一致的。3.3 中介者接口设计的三条经验写中介模式最核心的不是代码能不能跑而是中介者接口怎么设计。接口设计得合理后续维护才能轻松。我个人踩过不少坑之后总结出三条非常实用的经验。第一中介者的方法命名应该围绕业务事件而不是围绕组件调用。很多新手会把中介者写成notifySearchBar()、notifyTable()这种针对组件的方法这样一来中介者反而成了组件之间的传话人新增一个组件还要新增一堆方法。更好的做法是按事件命名比如onSearchKeywordChanged、onPageChanged、onRowSelected中介者的方法名描述的是发生了什么而不是要去通知谁。这两个的区别决定了中介者是主动协调者还是被动传声筒。第二中介者内部的执行顺序非常关键。比如搜索时先重置页码到第一页再发起请求请求返回后先更新表格数据再更新总条数状态。顺序错了用户看到的界面可能显示第0页或者总共0条数据但表格有3行。实际业务中经常在顺序上出问题。建议在中介者里用清晰的前后顺序去编排必要时可以加注释说明为什么这个在前、那个在后。第三不要在组件里偷偷绕过中介者。这个说起来容易做起来难。代码写得多了很容易图方便在某处直接调用另一个组件的方法觉得就这一次没关系。这种侥幸心理一旦开了头中介模式的价值就会慢慢被蚕食。我的建议是在评审代码时专门检查两条线一是组件的对外方法是否只被中介者调用二是组件内部是否没有直接引用其他组件的实例。守住这两条线星型结构才能保持干净。3.4 一个更能体现中介者价值的进阶场景如果你觉得上面这个例子还不够刺激再看一个更贴近后端业务系统的场景。假设你在开发一个风险交易拦截系统一笔交易进来后要经过身份校验、额度检查、风控规则评估、黑名单扫描、人工审核触发等多个环节。这些环节属于不同团队维护的独立服务但业务流程本身是强顺序的。如果让交易入口服务手动依次调用这些服务一旦中间某个环节判定风险较高需要人工复核你不仅要中断后续流程还可能要撤销前面已经完成的操作。更麻烦的是流程一变比如要在额度检查之前先做黑名单扫描你得动交易入口服务的代码。用中介模式我们把这些校验服务全部注册成一个风控编排器交易入口只管把原始交易数据交给他。编排器内部维护流程顺序和决策逻辑它可以动态决定哪些环节需要执行、哪个环节发现异常就中断、中断后要不要触发补偿操作。这样交易入口的职责非常纯净编排器的逻辑也可以单独测试。class RiskMediator { constructor(private steps: Array{ id: string; check: (tx: Transaction) CheckResult }) {} evaluate(tx: Transaction): Decision { const records []; for (const step of this.steps) { const result step.check(tx); records.push(result); if (!result.passed) { return { status: REJECTED, reason: result.reason, records }; } if (result.needManualReview) { return { status: MANUAL_REVIEW, records }; } } return { status: APPROVED, records }; } }核心的价值在这里你可以通过调整步骤列表来改变流程而无须修改业务入口代码。公司的风控策略经常变化有时候要新增一个渠道风控维度有时候要调整检查顺序。有了中介者这一层编排壳这些策略变更都被收敛到一处对接方根本感知不到。对于服务数量多、策略频繁调整的业务系统来说稳定接口 可变流程这个组合是非常实用的。4. 中介模式的利弊边界与使用判断4.1 优点与代价没有银弹中介模式在处理复杂交互时能大幅降低耦合度但它不是万能的它有自己的代价而且这个代价非常具体。优点方面首先是解耦效果立竿见影。原来组件A要引用B、C、D现在A只依赖中介者新增组件E时只需要在中介者里加一个引用不用去改动A、B、C、D。其次是交互逻辑集中管理流程的统一编排、顺序控制都在一个类里调试问题时可以只看一个文件而不是沿着调用链一路追查。最后是复用性更好组件更趋向于纯粹——搜索栏不知道分页器的存在它在其他页面也能被独立复用到同类交互里。代价方面第一个问题是中介者本身容易膨胀。一个页面所有交互逻辑都塞进去几百行、几千行都是很常见的时间久了会成为新的维护痛点。我见过最夸张的一个中介类从事件监听、数据转换、UI更新到权限判断全都有方法超过三十个那已经不是中介者了是一个没有纪律的上帝类。第二个问题是性能上多了一层间接性。理论上直接调用比经过中介者转发快虽然现代计算机上这点开销通常可以忽略但在高频循环场景中确实要留意。第三个问题是如果交互本身就简单——两个组件之间一对一互相调用就能讲清楚——那么引入中介者属于过度设计反而让简单的逻辑变得绕。4.2 什么时候用什么时候别用判断要不要上中介模式我的经验是看这几个信号信号一对象之间的交互超过三对以上的互相引用代码里出现一个类持有四个以上其他类实例并直接调用的现象。信号二交互规则经常变。今天要先校验这个明天要先触发那个规则的变动频率高到修改调用链成为周常任务。信号三希望模块具备独立可复用的能力。不想让组件知道太多业务全貌而是希望组件保持单一职责方便在多个地方复用。信号四交互涉及多个步骤的顺序协调并且中间有某一环节失败后终止流程、触发补偿这类复杂决策。反过来如果是两个组件简单联动比如一个按钮点击之后只改变另一个标签的文本直接引用清清楚楚完全不需要中介者。举手表态的时候你不会为了一次握手专门找个中间人同一个道理。4.3 和其他几个模式的边界辨析中介模式这个概念在面试和实际代码评审中经常被拿来和其他几个模式做对比容易混。我用自己的理解把它们捋一遍方便你横向比较。中介者模式 vs 观察者模式观察者模式是一对多的通知机制一个发布者发出消息多个订阅者各自响应。中介者模式是多对多的集中协调大量的交互关系压缩到一个协调者内部。两者的核心区别在于消息从哪来、流向哪去——观察者模式是被观察对象主动通知订阅者之间互不通信中介者模式是各个参与者主动报告给中介者由中介者决定下一步动作。实践中中介者模式内部经常用观察者模式来实现事件分发两者不是非此即彼的对立关系。中介者模式 vs 门面模式门面模式是对外暴露简化接口它把子系统的复杂性藏在门面后面但子系统之间依然可以直接交互。中介者模式是对内统一仲裁它真正改变了子系统之间的交互方式。门面隐藏复杂性中介者消除耦合。门面是单方向的客户端面朝门面调用子系统内部该怎样还怎样中介者是双向的各个同事对象都向中介者汇报中介者也反向指挥各个同事。中介者模式 vs 命令模式命令模式把请求封装成独立的对象使发送方和接收方解耦强调动作可延迟、可排队、可撤销。中介者模式强调的是流程协调它协调多个对象的交互。它们可以配合使用——中介者内部执行的每一步可以作为命令对象封装起来让编排逻辑具备可回滚的潜力。我在风控编排器的例子里就常用命令模式包一层每一步校验都能单独为失败做补偿。5. 实战中的常见坑与排查经验5.1 常见问题速查表先给你一张速查表下面每个问题我再展开说问题现象可能原因解决方向中介者类越来越大改动频繁流程逻辑和UI逻辑混杂按业务维度拆分多个中介者或分层编排组件之间有人绕过中介者直连图省事不走协调通道代码评审重点检查做静态扫描约束事件顺序错乱界面状态显示不对中介者内部步骤顺序设计不佳梳理业务流程把关键顺序写清楚中介者里到处都是监听器注册内存泄漏组件销毁时没有反注册提供 dispose 方法统一清理订阅关系新增事件要改一堆接口中介者也变了味道中介者接口粒度太粗或太细用事件对象封装参数避免方法爆炸逻辑一乱就怀疑中介模式想推翻重写不等于拆分不够、职责不清先梳理中介者内部职责边界再动手改5.2 我的日常编码清单在实际开发中我每写一个新中介者脑子里都会过一遍下面这个清单算是给自己的质检环节。首先明确界定哪些事归中介者管、哪些事不归它管。中介者管的是流程协调不管具体业务实现。让它去拼SQL、写缓存、组装复杂数据模型都是越权。我曾经在一个中介者里把数据拉取和数据组装都写了进去结果后面想复用这层协调逻辑根本没法用因为里面全是具体业务细节。中介者应该尽量薄只编排、不实现。其次给中介者设计清晰的从入到出的数据流。不要让组件直接把中间过程的数据丢给中介者最好定义事件对象做载体。比如定义SearchEvent、PageChangeEvent、RowSelectEvent每个事件里带上必要参数。这样中介者的方法签名稳定新增参数不破坏已有调用加字段对方法签名基本无感。第三保证组件销毁时能解除和中介者的绑定。很多系统内存泄漏都是因为组件创建时往中介者注册了一堆回调销毁时忘了反注册。用得多了我就养成了习惯中介者提供一个reset()或者dispose()方法统一清空事件订阅和组件引用。React类组件在componentWillUnmount里调这是底线级别的卫生习惯。5.3 从团队协作角度聊聊落地阻力技术问题好解决真正让中介模式落地难的是团队协作层面。常见的阻力有这么几种一种是觉得多了一层不直观。开发者习惯了直接调用看到this.mediator.xxx()总觉得绕了一圈。这种观念要扭转靠的不是讲大道理而是让团队在真实代码里尝到甜头。我做过的项目里单元测试最容易体现价值直接调用满天飞的代码测一个交互要mock五六个对象用中介者重构后mock一个中介者就够了测试代码量直接砍半。这种体验一出来团队自己就会接受。另一种是没有守门人习惯慢慢回归。即便上了中介模式代码里还是会冒出直连调用的苗头。我在团队里定了一条规矩任何组件之间的通信都不允许直接调用必须经过统一的协调层代码评审时专门盯着这一点。配合简单的依赖扫描脚本把组件间直接互相持有实例的行为当作坏味道自动标红。一套流程坚持下来比反复强调大家注意解耦有效得多。5.4 中介者模式的演化变体最后聊一个进阶话题中介模式在实际演进中会出现几种常见变体你遇到别觉得陌生。简化版事件总线。它的特点是不持有组件实例组件只需要向一个全局事件总线发布事件再由总线按事件类型分发给订阅者。这种形式很灵活但缺点也很明显——流程隐式化执行顺序不容易从代码结构上看出来。适合交互不特别复杂、但散落点很多的场景。小规模团队搭内部平台用这种事件总线起步是性价比很高的。分层中介者。当一个中介者确实大到无法管理时拆成多个小中介者再由一个总协调者管理小中介者。比如后台系统里有表格协调器负责表格内部交互筛选协调器负责筛选区交互页面总协调器负责把表格和筛选串起来。拆分的价值在于职责分离每个中介者只关心自己领域的规则跨领域协作由上层统一协调。可插拔策略中介者。中介者内部的执行步骤抽象成策略接口运行时动态装配。风控编排器就是一个典型例子每个校验步骤都可以是独立策略生产环境通过配置中心决定流程顺序。这种变体把中介模式从静态编码协调逻辑推向动态可配协调逻辑在大型企业系统里尤其有用。6. 我的一点个人看法市面上聊设计模式的资料浩如烟海但我始终觉得像中介模式这种看似简单的东西真正要嚼透它靠的还是多写多踩。模式本身只是工具关键是你什么时候掏出来用、怎么用着顺手。我自己用过它之后最大的感受是一个项目真正变得好维护靠的不是所有代码都用上高大上的模式而是每个地方用合适的模式恰到好处。中介模式不是银弹它解决的是多对多网状交互这个特定问题当一个问题不需要它时硬套上去反而碍手碍脚。如果你刚好有机会在一个交互很复杂的系统里做重构我建议你从一个小模块开始尝试把中介者写出来跑通主流程再对比一下重构前后的改动量和测试成本。我敢打赌只要你坚持两个迭代就能体会到它带来的那种清爽感——至少在改代码的时候不用再提心吊胆担心牵一发动全身了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →