桥接模式:让抽象与实现各自飞
桥接模式,说白了就是把「抽象」和「实现」拆开,让它们能各自独立变化。你想想看,我们做接口适配时,最头疼的是什么?就是需求一变,整个继承体系都得跟着改。桥接模式就是来解决这个问题的。
我记得刚工作那会儿,接手过一个老项目。对方系统升级了通信协议,我们这边得跟着适配。原来的代码把「消息类型」和「传输方式」绑死在继承树里,改一个地方,十几个子类都得动。那叫一个痛苦。后来重构时用了桥接模式,世界清净了。
桥接模式的核心:两个独立维度
桥接模式的核心思想,就是把一个事物中「会变化的两条线」拆开。一条线是抽象部分,另一条是实现部分。它们通过组合关系连接,而不是继承。
举个例子:
- 抽象部分:消息的类型(普通消息、加密消息、签名消息)
- 实现部分:消息的发送方式(HTTP、MQ、WebSocket)
这两个维度都会独立变化。今天加一种消息类型,明天换一种传输方式。如果用继承,就是 3×3=9 个子类。后天再加一种,就是 4×4=16 个。你受得了吗?
桥接模式的做法是:
- 把「消息类型」做成抽象类,里面持有「发送方式」的引用
- 把「发送方式」做成接口,各种传输方式去实现它
- 消息类型要发消息时,直接调持有的发送方式接口
这样,新增消息类型不影响发送方式,新增发送方式也不影响消息类型。两个维度各自独立扩展。
核心公式:桥接模式 = 抽象维度 + 实现维度 + 组合桥接
变化维度数 = M + N,而不是 M × N
抽象化与实现化:到底谁是谁?
很多初学者会搞混「抽象」和「实现」在这里的含义。我简单解释一下:
- 抽象化(Abstraction):定义「做什么」的接口或高层控制逻辑。比如「发送消息」这个行为,它定义了发送的流程:构建消息体、调用传输、处理结果。
- 实现化(Implementor):定义「怎么做」的底层接口。比如「通过 HTTP 发送」、「通过 MQ 发送」,它们只负责把数据发出去,不关心消息长什么样。
抽象化持有实现化的引用,调用它的方法来完成具体工作。这就是「桥」——抽象化这边调用,实现化那边执行。
| 角色 | 职责 | 变化原因 |
|---|---|---|
| Abstraction(抽象) | 定义高层接口,持有 Implementor | 业务需求变化(消息类型) |
| RefinedAbstraction(精确抽象) | 扩展抽象类的功能 | 具体业务场景 |
| Implementor(实现) | 定义底层接口 | 技术选型变化 |
| ConcreteImplementor(具体实现) | 实现底层接口 | 具体传输方式 |
独立变化的维度:怎么识别?
我在项目中判断是否该用桥接模式,就看一个问题:这个类是不是有两个以上会独立变化的方向?
常见的独立维度组合:
- 消息类型 × 传输方式
- 数据格式 × 存储介质
- 认证方式 × 密钥存储
- 加密算法 × 密钥来源
嗯,这里要注意:不是所有两个维度都适合拆。如果两个维度变化频率差不多,或者变化方向很少,用继承反而更直观。桥接模式有它的适用场景。
我的经验:当你在代码里看到「if-else」或「switch」判断两个以上条件时,往往就是桥接模式该出场的时候。比如:
if (type == "加密") {
if (transport == "HTTP") { ... }
else if (transport == "MQ") { ... }
}
这种代码,就是桥接模式要消灭的坏味道。
桥接模式在接口适配中的应用
接口适配的场景,天然适合桥接模式。为什么?因为接口适配通常涉及两个变化维度:
- 业务协议:不同接口的业务逻辑不同(查询、下单、通知)
- 通信协议:不同系统的通信方式不同(HTTP REST、gRPC、MQ)
我做过一个支付网关的适配层,就是典型的桥接模式。抽象层定义「支付」接口(支付、退款、查询),实现层对接不同支付渠道(、支付宝、银联)。
来看一段简化代码:
// 实现化接口:支付渠道
interface PaymentChannel {
boolean send(String request);
String receive();
}
// 具体实现:支付
class WechatChannel implements PaymentChannel {
public boolean send(String request) {
// 调用 HTTP API
return true;
}
public String receive() {
// 解析回调
return "result";
}
}
// 抽象化:支付业务
abstract class Payment {
protected PaymentChannel channel;
protected Payment(PaymentChannel channel) {
this.channel = channel;
}
abstract boolean pay(String orderId, double amount);
abstract boolean refund(String orderId);
}
// 精确抽象:普通支付
class NormalPayment extends Payment {
public NormalPayment(PaymentChannel channel) {
super(channel);
}
boolean pay(String orderId, double amount) {
String req = buildPayRequest(orderId, amount);
return channel.send(req);
}
boolean refund(String orderId) {
String req = buildRefundRequest(orderId);
return channel.send(req);
}
}
你看,新增一个「支付宝渠道」,只需要加一个 AlipayChannel implements PaymentChannel。新增一个「签约支付」业务,只需要加一个 ContractPayment extends Payment。互不干扰。
我曾经踩过的坑:桥接模式虽然好,但别滥用。如果你的抽象和实现只有一种变化可能,或者变化频率极低,用桥接反而增加复杂度。我见过有人把只有一种实现方式的接口也拆成桥接,结果代码绕了三层,调试时追得头晕。记住:模式是为解决问题服务的,不是为了炫技。
桥接模式的结构图
下面这张图,把桥接模式的核心结构画出来了。你看一眼就能明白:抽象和实现之间,就是通过一条「桥」连接的。
什么时候用桥接模式?
我总结了几条判断标准,你可以对照着看:
- 两个维度独立变化:一个类的变化原因有两个以上,且互不依赖
- 避免类爆炸:继承会导致子类数量呈乘积增长
- 运行时切换:需要在运行时动态选择实现方式
- 实现细节对抽象透明:抽象层不需要知道底层怎么实现的
接口适配的场景,几乎都满足这些条件。你想想看,对接第三方系统时,对方的接口协议、通信方式、数据格式,哪个不是独立变化的?今天对接支付,明天对接支付宝,后天可能还要对接 Stripe。桥接模式就是为这种场景量身定做的。
一个小建议:刚开始用桥接模式时,别急着把代码全拆了。先找出最常变化的两个维度,从它们开始拆。拆完之后,你会发现新增功能时,改动的代码量明显减少。那种「改一处,全盘皆动」的恐惧感,会慢慢消失。