设计模式 22 · 三个冷门模式:中介者、访问者、解释器 行为型的最后三个模式——中介者(Mediator)、访问者(Visitor)、解释器(Interpreter)——我们放在一篇里集中讲。为什么合并?因为它们是二十三式里最冷门、适用面最窄的三个:中介者容易被滥用成上帝类,访问者结构复杂且限制苛刻,解释器则是公认几乎用不到的一个。把它们放一起,不是敷衍,而是要传递一个和全系列一脉相承的态度:这三个模式,你更需要知道的是它们解决什么问题、以及为什么大多数时候你不该用它们,而不是把它们的模板背下来到处套。所以这一篇的写法和前面不同:每个模式我只讲清三件事——它解决什么问题、核心怎么做、以及为什么适用面这么窄,点到为止,不铺长。读完你能认出它们、知道它们的定位,并在极少数真正需要的场景里想起它们,就够了。目录中介者:让一群对象别再两两纠缠访问者:数据结构稳定,操作却不断加解释器:为一种小语言定义文法三个模式的共同点:适用面都很窄一、中介者:让一群对象别再两两纠缠中介者模式解决什么问题?当一堆对象之间两两直接通信、相互引用时,它们会织成一张乱麻般的网——每个对象都要认识一大堆别的对象,任何一个改动都可能牵连一片。中介者的思路:引入一个中介者对象,让所有对象都只跟中介者通信,而不再彼此直接引用,把网状的多对多关系,变成星型的一对多。一个订单场景的例子:下单页的组件联动。下单页上有一堆相互关联的组件:选了优惠券,总金额要变;改了收货地址,运费要变,运费变了总金额又要变;改了商品数量,金额、运费、优惠资格全要重算……如果每个组件都直接持有并调用其他组件,就是一张可怕的网。用中介者,让每个组件的变化都通知下单页中介者,由中介者统一协调这个变了、该让哪些组件跟着更新:// 中介者:统一协调各组件的联动publicclassOrderPageMediator{privateCouponComponentcoupon;privateAddressComponentaddress;privateAmountComponentamount;// ... 持有各组件// 某个组件变了,通知中介者,由它决定联动谁publicvoidchanged(Componentsource){if(sourcecoupon){amount.recalculate();}if(sourceaddress){amount.recalculate();/* 还有运费等 */}// 协调逻辑集中在这里}}// 组件不再直接调别的组件,只通知中介者publicclassCouponComponent{privateOrderPageMediatormediator;publicvoidselect(){// ... 选优惠券mediator.changed(this);// 只告诉中介者我变了,不管谁联动}}好处很清楚:组件之间彻底解耦了,每个组件只认识中介者,不认识其他组件;所有的联动规则集中在中介者一处,清晰可查;加一个新组件,只需让它接入中介者。这和第 11 篇的外观模式有点像(都是加一个中间层),但意图不同:外观是单向的(简化外部对子系统的调用),中介者是双向的(协调内部一群对等对象的相互通信)。为什么它适用面窄、还容易被滥用?最大的风险是——中介者本身会膨胀成一个上帝类。因为所有协调逻辑都往中介者里塞,组件越多、联动越复杂,中介者就越臃肿,最后变成一个几百上千行、什么都管、谁都不敢碰的巨无霸。你只是把分散在各处的耦合,转移成了高度集中在一个类里的复杂度。所以中介者的适用场景很挑:只有当对象间确实是多对多的复杂交互、且交互逻辑适合集中管理时才用;如果对象关系本来就简单、或只是单向依赖,用它纯属自找麻烦。现实里 GUI 框架的表单联动、聊天室(用户通过服务器中转消息,而非两两直连)是它比较合适的场景。二、访问者:数据结构稳定,操作却不断加访问者模式解决什么问题?当你有一个稳定的数据结构(元素的种类基本固定),但需要不断给它增加新的操作时,访问者能让你在不修改元素类的前提下,新增操作。它的核心手法有点绕:把操作从元素类里抽出来,封装成一个个独立的访问者;元素只提供一个accept(visitor)方法,把自己交给访问者去处理。一个订单场景的例子:对订单集合做多种统计/导出操作。假设订单里有不同类型的元素(实物商品、虚拟商品、服务),你需要对它们做各种操作:算总价、导出 Excel、生成报表、统计重量……而且这些操作会不断新增。如果把每个操作都写进元素类,那每加一个操作,所有元素类都要改。访问者反过来:// 访问者接口:为每种元素类型定义一个 visit 方法publicinterfaceOrderVisitor{voidvisit(PhysicalItemitem);voidvisit(VirtualItemitem);}// 每个操作是一个访问者publicclassPriceVisitorimplementsOrderVisitor{voidvisit(PhysicalItemitem){/* 算实物价 */}voidvisit(VirtualItemitem){/* 算虚拟价 */}}publicclassExportVisitorimplementsOrderVisitor{/* 导出逻辑 */}// 元素只提供 accept,把自己交给访问者publicclassPhysicalItemimplementsItem{publicvoidaccept(OrderVisitorvisitor){visitor.visit(this);}}于是新增一个操作(比如统计重量),只需写一个新的WeightVisitor,所有元素类一个字都不用改。这就是访问者的价值——把操作这个变化维度,从元素类里彻底解放出来。为什么它适用面极窄?因为它有一个致命的倾斜性(和第 4 篇抽象工厂的倾斜性异曲同工):它对增加操作友好,但对增加元素类型极其敌视。新增一个操作 → 加一个访问者,爽;但新增一种元素类型(比如加个ServiceItem)→ 你得回去修改每一个访问者接口和所有实现,加上对新元素的visit方法——牵一发动全身,彻底违反开闭。所以访问者的适用前提非常苛刻:元素种类必须高度稳定(几乎不变),而操作会频繁增加。满足这个前提的场景本就不多(编译器的 AST 处理是经典例子:语法树节点类型固定,但要对它做类型检查、代码生成、优化等很多种操作)。加上它那套accept/visit的双分派结构本身就绕、可读性差,所以业务开发里极少用到。看到它能认出、知道它解决稳定结构 多变操作即可。三、解释器:为一种小语言定义文法解释器模式解决什么问题?当你需要处理一种简单的、自定义的小语言(比如一套规则表达式、一种查询语法)时,解释器提供一种方式:为这个语言定义一套文法,把每条文法规则表示成一个类,然后用这些类的对象组成一棵语法树,通过遍历这棵树来解释执行表达式。一个订单场景的例子:优惠规则表达式。假设运营想灵活配置优惠规则,比如金额100 AND 会员这样的表达式。解释器会把它拆成一棵树:AND是一个节点,左边是金额100(一个表达式),右边是会员(一个表达式),每种表达式是一个实现了interpret()的类,递归地解释求值:publicinterfaceExpression{booleaninterpret(Contextctx);// 解释:在给定上下文下求值}// 与 表达式publicclassAndExpressionimplementsExpression{privateExpressionleft,right;publicbooleaninterpret(Contextctx){returnleft.interpret(ctx)right.interpret(ctx);// 递归解释子表达式}}// 金额大于 表达式、是会员 表达式 ... 各是一个类你会发现,它的结构本质上就是第 10 篇组合模式(树形结构 递归)——解释器可以看作组合模式在’语言解释’这个特定场景的应用。为什么它几乎用不到?三个原因:其一,适用面极其狭窄——只有需要解释一种自定义小语言这一种场景才用得上,而这种需求本就罕见。其二,类爆炸——文法规则稍微一多,就要定义一大堆表达式类,复杂文法下根本维护不了。其三,有更好的替代——真要处理复杂表达式/语言,现实中都用成熟的工具:正则表达式、脚本引擎(如 Groovy、JS 引擎)、规则引擎(如 Drools)、或专门的解析器生成器(ANTLR),它们比手写解释器强大和健壮得多。所以解释器是二十三式里公认最少用的一个。它的价值更多是思想启发(理解语言是怎么被解析执行的),而非实战工具。了解它是什么、知道真要做这事有更好的轮子,就足够了。四、三个模式的共同点:适用面都很窄把这三个模式放一起收个尾,它们其实共享一个特征,也正是我们合并讲的原因:适用场景都非常窄,且都有明显的反噬风险。模式解决什么为什么少用中介者多对多交互 → 集中协调中介者易膨胀成上帝类访问者稳定结构 多变操作加元素类型就牵一发动全身;结构绕解释器解释自定义小语言场景罕见 类爆炸 有更好的轮子这三个模式,恰恰是全系列别过度设计这条主线最好的注脚。它们不是高级“厉害的模式,恰恰相反,它们是最需要克制的模式——因为它们结构复杂、限制苛刻,一旦用错场景,带来的复杂度远超收益。真正成熟的做法,不是我学会了访问者所以要找机会用”,而是我知道访问者的存在,但我清楚我这个场景的元素类型还会变,所以我不用它。知道一个模式什么时候不该用,和知道它怎么用,同等重要——甚至更重要。用一张图把这三个冷门模式的定位和别用它的信号钉在一起:小结。这一篇把行为型里最冷门的三个模式集中讲了:中介者用一个中介者集中协调一群对象的多对多交互,把网状耦合变星型,但要警惕它膨胀成上帝类;访问者把操作从稳定的数据结构里抽出来、以便不改元素就新增操作,但它对新增元素类型极度敌视、结构也绕,业务里极少用;解释器为一种自定义小语言建语法树来解释执行,但场景罕见、易类爆炸、且有正则/脚本引擎/规则引擎等更好的替代,是最少用的一个。它们共同的教训呼应全系列的主线——这些模式最需要的是克制,知道什么时候不该用它们,比会用更重要。下一篇是整个「设计模式拆解」系列的收尾总纲:我们会盘点 JDK/Spring/MyBatis 源码里的模式、集中辨析那些容易混淆的成对模式、再谈一次过度设计,把二十三个模式和七大原则串成一张完整的地图。