设计模式 22 · 三个冷门模式:中介者、访问者、解释器

行为型的最后三个模式------中介者(Mediator)、访问者(Visitor)、解释器(Interpreter) ------我们放在一篇里集中讲。为什么合并?因为它们是二十三式里最冷门、适用面最窄 的三个:中介者容易被滥用成"上帝类",访问者结构复杂且限制苛刻,解释器则是公认几乎用不到的一个。把它们放一起,不是敷衍,而是要传递一个和全系列一脉相承的态度:这三个模式,你更需要知道的是"它们解决什么问题、以及为什么大多数时候你不该用它们",而不是把它们的模板背下来到处套。

所以这一篇的写法和前面不同:每个模式我只讲清三件事------它解决什么问题、核心怎么做、以及为什么适用面这么窄,点到为止,不铺长。读完你能认出它们、知道它们的定位,并在极少数真正需要的场景里想起它们,就够了。

目录

  1. 中介者:让一群对象别再两两纠缠
  2. 访问者:数据结构稳定,操作却不断加
  3. 解释器:为一种"小语言"定义文法
  4. 三个模式的共同点:适用面都很窄

一、中介者:让一群对象别再两两纠缠

中介者模式解决什么问题? 当一堆对象之间两两直接通信、相互引用 时,它们会织成一张乱麻般的网------每个对象都要认识一大堆别的对象,任何一个改动都可能牵连一片。中介者的思路:引入一个"中介者"对象,让所有对象都只跟中介者通信,而不再彼此直接引用,把"网状"的多对多关系,变成"星型"的一对多。

一个订单场景的例子:下单页的组件联动。 下单页上有一堆相互关联的组件:选了优惠券,总金额要变;改了收货地址,运费要变,运费变了总金额又要变;改了商品数量,金额、运费、优惠资格全要重算......如果每个组件都直接持有并调用其他组件,就是一张可怕的网。用中介者,让每个组件的变化都通知"下单页中介者",由中介者统一协调"这个变了、该让哪些组件跟着更新":

java 复制代码
// 中介者:统一协调各组件的联动
public class OrderPageMediator {
    private CouponComponent coupon;
    private AddressComponent address;
    private AmountComponent amount;
    // ... 持有各组件

    // 某个组件变了,通知中介者,由它决定联动谁
    public void changed(Component source) {
        if (source == coupon)  { amount.recalculate(); }
        if (source == address) { amount.recalculate(); /* 还有运费等 */ }
        // 协调逻辑集中在这里
    }
}

// 组件不再直接调别的组件,只通知中介者
public class CouponComponent {
    private OrderPageMediator mediator;
    public void select() {
        // ... 选优惠券
        mediator.changed(this);   // 只告诉中介者"我变了",不管谁联动
    }
}

好处很清楚:组件之间彻底解耦了 ,每个组件只认识中介者,不认识其他组件;所有的联动规则集中在中介者一处,清晰可查;加一个新组件,只需让它接入中介者。这和第 11 篇的外观模式 有点像(都是加一个中间层),但意图不同:外观是单向 的(简化外部对子系统的调用),中介者是双向的(协调内部一群对等对象的相互通信)。

为什么它适用面窄、还容易被滥用? 最大的风险是------中介者本身会膨胀成一个"上帝类" 。因为所有协调逻辑都往中介者里塞,组件越多、联动越复杂,中介者就越臃肿,最后变成一个几百上千行、什么都管、谁都不敢碰的巨无霸。你只是把"分散在各处的耦合",转移成了"高度集中在一个类里的复杂度"。所以中介者的适用场景很挑:只有当对象间确实是"多对多的复杂交互"、且交互逻辑适合集中管理时才用;如果对象关系本来就简单、或只是单向依赖,用它纯属自找麻烦。 现实里 GUI 框架的表单联动、聊天室(用户通过服务器中转消息,而非两两直连)是它比较合适的场景。

二、访问者:数据结构稳定,操作却不断加

访问者模式解决什么问题? 当你有一个稳定的数据结构 (元素的种类基本固定),但需要不断给它增加新的操作 时,访问者能让你在不修改元素类 的前提下,新增操作。它的核心手法有点绕:把"操作"从元素类里抽出来,封装成一个个独立的"访问者";元素只提供一个 accept(visitor) 方法,把自己"交给"访问者去处理。

一个订单场景的例子:对订单集合做多种统计/导出操作。 假设订单里有不同类型的元素(实物商品、虚拟商品、服务),你需要对它们做各种操作:算总价、导出 Excel、生成报表、统计重量......而且这些操作会不断新增。如果把每个操作都写进元素类,那每加一个操作,所有元素类都要改。访问者反过来:

java 复制代码
// 访问者接口:为每种元素类型定义一个 visit 方法
public interface OrderVisitor {
    void visit(PhysicalItem item);
    void visit(VirtualItem item);
}

// 每个操作是一个访问者
public class PriceVisitor implements OrderVisitor {
    void visit(PhysicalItem item) { /* 算实物价 */ }
    void visit(VirtualItem item)  { /* 算虚拟价 */ }
}
public class ExportVisitor implements OrderVisitor { /* 导出逻辑 */ }

// 元素只提供 accept,把自己交给访问者
public class PhysicalItem implements Item {
    public void accept(OrderVisitor visitor) { visitor.visit(this); }
}

于是新增一个操作(比如"统计重量"),只需写一个新的 WeightVisitor,所有元素类一个字都不用改 。这就是访问者的价值------把"操作"这个变化维度,从元素类里彻底解放出来

为什么它适用面极窄? 因为它有一个致命的"倾斜性"(和第 4 篇抽象工厂的倾斜性异曲同工):它对"增加操作"友好,但对"增加元素类型"极其敌视。 新增一个操作 → 加一个访问者,爽;但新增一种元素类型(比如加个 ServiceItem)→ 你得回去修改每一个 访问者接口和所有实现,加上对新元素的 visit 方法------牵一发动全身,彻底违反开闭。所以访问者的适用前提非常苛刻:元素种类必须高度稳定(几乎不变),而操作会频繁增加。 满足这个前提的场景本就不多(编译器的 AST 处理是经典例子:语法树节点类型固定,但要对它做类型检查、代码生成、优化等很多种操作)。加上它那套 accept/visit 的"双分派"结构本身就绕、可读性差,所以业务开发里极少用到。看到它能认出、知道它解决"稳定结构 + 多变操作"即可。

三、解释器:为一种"小语言"定义文法

解释器模式解决什么问题? 当你需要处理一种简单的、自定义的"小语言" (比如一套规则表达式、一种查询语法)时,解释器提供一种方式:为这个语言定义一套文法,把每条文法规则表示成一个类,然后用这些类的对象组成一棵"语法树",通过遍历这棵树来"解释执行"表达式。

一个订单场景的例子:优惠规则表达式。 假设运营想灵活配置优惠规则,比如 "金额>100 AND 会员" 这样的表达式。解释器会把它拆成一棵树:AND 是一个节点,左边是 金额>100(一个表达式),右边是 会员(一个表达式),每种表达式是一个实现了 interpret() 的类,递归地解释求值:

java 复制代码
public interface Expression {
    boolean interpret(Context ctx);   // 解释:在给定上下文下求值
}
// "与" 表达式
public class AndExpression implements Expression {
    private Expression left, right;
    public boolean interpret(Context ctx) {
        return left.interpret(ctx) && right.interpret(ctx);   // 递归解释子表达式
    }
}
// "金额大于" 表达式、"是会员" 表达式 ... 各是一个类

你会发现,它的结构本质上就是第 10 篇组合模式(树形结构 + 递归)------解释器可以看作"组合模式在'语言解释'这个特定场景的应用"。

为什么它几乎用不到? 三个原因:其一,适用面极其狭窄 ------只有"需要解释一种自定义小语言"这一种场景才用得上,而这种需求本就罕见。其二,类爆炸 ------文法规则稍微一多,就要定义一大堆表达式类,复杂文法下根本维护不了。其三,有更好的替代 ------真要处理复杂表达式/语言,现实中都用成熟的工具:正则表达式、脚本引擎(如 Groovy、JS 引擎)、规则引擎(如 Drools)、或专门的解析器生成器(ANTLR),它们比手写解释器强大和健壮得多。所以解释器是二十三式里公认最少用 的一个。它的价值更多是"思想启发"(理解语言是怎么被解析执行的),而非实战工具。了解它是什么、知道"真要做这事有更好的轮子",就足够了。

四、三个模式的共同点:适用面都很窄

把这三个模式放一起收个尾,它们其实共享一个特征,也正是我们合并讲的原因:适用场景都非常窄,且都有明显的"反噬"风险。

模式 解决什么 为什么少用
中介者 多对多交互 → 集中协调 中介者易膨胀成"上帝类"
访问者 稳定结构 + 多变操作 加元素类型就牵一发动全身;结构绕
解释器 解释自定义小语言 场景罕见 + 类爆炸 + 有更好的轮子

这三个模式,恰恰是全系列"别过度设计 "这条主线最好的注脚。它们不是"高级""厉害"的模式,恰恰相反,它们是最需要克制 的模式------因为它们结构复杂、限制苛刻,一旦用错场景,带来的复杂度远超收益。真正成熟的做法,不是"我学会了访问者所以要找机会用",而是"我知道访问者的存在,但我清楚我这个场景的元素类型还会变,所以我不用 它"。知道一个模式什么时候 不该用,和知道它怎么用,同等重要------甚至更重要。

用一张图把这三个"冷门模式"的定位和"别用它"的信号钉在一起:


小结 。这一篇把行为型里最冷门的三个模式集中讲了:中介者 用一个中介者集中协调一群对象的多对多交互,把网状耦合变星型,但要警惕它膨胀成上帝类;访问者 把"操作"从稳定的数据结构里抽出来、以便不改元素就新增操作,但它对"新增元素类型"极度敌视、结构也绕,业务里极少用;解释器 为一种自定义小语言建语法树来解释执行,但场景罕见、易类爆炸、且有正则/脚本引擎/规则引擎等更好的替代,是最少用的一个。它们共同的教训呼应全系列的主线------这些模式最需要的是克制,知道什么时候不该用它们,比会用更重要 。下一篇是整个「设计模式拆解」系列的收尾总纲:我们会盘点 JDK/Spring/MyBatis 源码里的模式、集中辨析那些容易混淆的成对模式、再谈一次过度设计,把二十三个模式和七大原则串成一张完整的地图。

相关推荐
andongni2031 小时前
Spring Boot基础应用开发与部署
java·spring boot·后端
lupai1 小时前
增值税发票 OCR 识别 API 新手接入指南
java·前端·ocr
TELL5211 小时前
Sonar质量门禁
java
java修仙传1 小时前
从网页禅道到 AI 能调用的工具:我的禅道 MCP 实现思路分享
java·人工智能·python·ai应用·mcp开发
HAYDENR1 小时前
依赖数据迁移工具做增量同步有哪些易错点?调整数据迁移工具策略怎么保证断点续传可靠?
java·大数据·数据库
IT利刃出鞘2 小时前
SpringBoot--解决@Valid放在接口的List上时无效的问题
java·spring
mqiqe2 小时前
AgentScope Java Harness:2. 上下文压缩:让长期 Agent 永不“失忆“
java·开发语言
梦想的旅途22 小时前
企业微信API实战:Python自动化消息推送
java·前端·python·自动化·企业微信
YOU OU2 小时前
RabbitMQ运维
java·rabbitmq·java-rabbitmq