每次讲 Bridge 模式,底下总有人小声说"这不就是策略模式吗"。换个接口实现而已,有什么难的。
但你去问"JDBC 的 DriverManager 和 Driver 之间是什么关系",同一个能说出一堆设计模式的人,大部分答不上来。JDBC 就是 Bridge 模式的标准教科书案例,用了 30 年,每天都在你代码里跑,你却没认出它来。
Bridge 跟 Strategy 的区别不在于代码写法,在于它解决的问题维度。Strategy 是"一个维度上换实现",Bridge 是"两个维度同时变"。这个区别搞不清,你写的"Bridge"要么退化成 Strategy,要么变成继承爆炸的怪物。
先看一个让你继承爆炸的真实场景
假设你在做一个消息通知系统,需要支持两种维度:
- 维度一:消息类型(短信、邮件、站内信、Push)
- 维度二:发送渠道(阿里云、腾讯云、自建网关)
最直觉的写法是继承:
markdown
Message
├── SMSMessage
│ ├── AliyunSMSMessage
│ ├── TencentSMSMessage
│ └── SelfHostedSMSMessage
├── EmailMessage
│ ├── AliyunEmailMessage
│ ├── TencentEmailMessage
│ └── SelfHostedEmailMessage
├── PushMessage
│ ├── AliyunPushMessage
│ ├── TencentPushMessage
│ └── SelfHostedPushMessage
└── InAppMessage
├── AliyunInAppMessage
├── TencentInAppMessage
└── SelfHostedInAppMessage
4 种消息类型 × 3 个发送渠道 = 12 个类。加一种消息类型变成 15 个,加一个渠道变成 16 个。子类数量 = 类型数 × 渠道数,指数级膨胀。
这就是 Bridge 要解决的经典问题:两个独立变化的维度,用继承会把它们绑死,用组合才能解耦。
Bridge 的解法:把一个维度抽成接口
把"发送渠道"这个维度抽成接口,消息类型持有它的引用:
java
// 实现维度:发送渠道
public interface MessageSender {
void send(String to, String content);
}
public class AliyunSender implements MessageSender {
public void send(String to, String content) {
// 调阿里云 SMS/邮件/Push API
}
}
public class TencentSender implements MessageSender {
public void send(String to, String content) {
// 调腾讯云 API
}
}
public class SelfHostedSender implements MessageSender {
public void send(String to, String content) {
// 走自建网关
}
}
// 抽象维度:消息类型
public abstract class Message {
protected MessageSender sender; // 桥接点:持有实现维度
public Message(MessageSender sender) {
this.sender = sender;
}
public abstract void notify(String to, String content);
}
public class SMSMessage extends Message {
public SMSMessage(MessageSender sender) {
super(sender);
}
public void notify(String to, String content) {
// 短信特有的处理:字数截断、签名追加
String smsContent = truncateContent(content);
sender.send(to, smsContent);
}
}
public class EmailMessage extends Message {
public EmailMessage(MessageSender sender) {
super(sender);
}
public void notify(String to, String content) {
// 邮件特有的处理:HTML 模板、附件
String htmlContent = wrapHtml(content);
sender.send(to, htmlContent);
}
}
现在类数量变成 4 + 3 = 7 个。加一种消息类型加 1 个类,加一个渠道加 1 个类。两个维度独立扩展,互不影响。
Message 持有 MessageSender 的引用------这个引用就是"桥"。抽象维度(消息类型)通过这座桥调用实现维度(发送渠道),两者解耦但能通信。
JDBC:用了 30 年的 Bridge
JDBC 的设计就是标准的 Bridge 模式,只是它用了不同的名字:
抽象维度(Abstraction):java.sql.DriverManager / java.sql.Connection
实现维度(Implementor):java.sql.Driver(各数据库厂商实现)
你写 Java 代码连数据库,从来没有直接 new MysqlDriver() 或者 new OracleDriver()。你是通过 DriverManager.getConnection() 拿到 Connection,然后操作数据库。
java
// 你写的代码
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb", "root", "password"
);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
DriverManager 是抽象维度,Driver 是实现维度。MySQL 驱动、Oracle 驱动、PostgreSQL 驱动各自实现 java.sql.Driver 接口,通过 SPI 机制注册到 DriverManager 里。
java
// MySQL 驱动里的注册代码(com.mysql.cj.jdbc.Driver)
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
}
你换数据库的时候不用改业务代码,只换驱动 JAR 包和连接字符串。两个维度独立变化:JDBC API(抽象维度)升级不影响你用的驱动(实现维度),驱动版本更新不影响你写的 JDBC 调用代码。
这就是 Bridge 的工程价值------不是炫技,是让你的代码能跨数据库运行。
Bridge 和 Strategy 到底差在哪
代码结构上,Bridge 和 Strategy 长得几乎一样:都是一个类持有另一个接口的引用,通过组合代替继承。
区别在语义和场景:
Strategy 是"一个维度换实现":
- 你有一个排序功能,可以在运行时切换快速排序、归并排序、堆排序
- 排序策略是可替换的,但"排序"这个行为本身不变
- 策略对象通常是短生命周期的,用完就丢
Bridge 是"两个维度同时变":
- 你有消息类型和发送渠道两个独立变化的维度
- 两个维度都需要独立扩展,不是简单的"换一个实现"
- 桥接对象通常是长生命周期的,跟宿主对象同生共死
判断标准很简单:如果你只有一个变化维度,用 Strategy。如果你有两个独立变化维度,用 Bridge。如果你把 Bridge 用在只有一个维度的场景上,它就退化成了 Strategy------不是错,是没必要。
三个真实踩坑
坑一:把实现维度做成静态工具类
有人觉得 MessageSender 只是转发调用,做成接口太重了,直接用静态工具类:
java
public class SendUtils {
public static void sendByAliyun(String to, String content) { ... }
public static void sendByTencent(String to, String content) { ... }
}
然后在 SMSMessage 里 if (channel.equals("aliyun")) SendUtils.sendByAliyun(...)。
这就回到了 if-else 地狱,Bridge 的意义全部丢失。实现维度必须是接口 + 多实现,这样才能通过依赖注入替换,才能单元测试时 mock。
坑二:抽象维度知道太多实现细节
Bridge 的核心是抽象维度不该知道实现维度的细节。但有人写着写着就在抽象类里写了实现特定的逻辑:
java
public abstract class Message {
protected MessageSender sender;
public void notify(String to, String content) {
if (sender instanceof AliyunSender) {
// 阿里云限流:每秒最多 100 条
rateLimiter.acquire();
}
sender.send(to, content);
}
}
instanceof 一出现,Bridge 的抽象-实现分离就破了。限流逻辑应该放在 AliyunSender 内部,不该泄漏到抽象层。抽象维度只该定义"做什么",不关心"怎么做"。
坑三:桥接对象的生命周期管理
Bridge 模式里,抽象对象持有实现对象的引用。如果实现对象持有资源(连接池、文件句柄),生命周期管理就变重要了。
JDBC 的 Connection 就是个典型。Connection 是抽象维度,它内部持有驱动厂商提供的实现对象。Connection.close() 关的不只是你的连接,还有底层的网络连接和资源。
一个常见 bug:在连接池场景下,Connection.close() 不是真关连接,而是还回连接池。如果你自己实现 Bridge 模式时没考虑这种"归还而非销毁"的语义,资源泄漏是迟早的事。
什么时候该用 Bridge
不是所有两个维度的场景都需要 Bridge。判断标准:
- 两个维度都在独立变化,且未来大概率会扩展 → 用 Bridge
- 两个维度中一个稳定一个变化 → 只对变化维度用 Strategy 就够了
- 两个维度都稳定 → 直接继承,别过度设计
JDBC 适合 Bridge 是因为:API 维度(Connection/Statement/ResultSet)和驱动维度(MySQL/Oracle/PostgreSQL)都在独立演进。JDBC 4.0 到 4.3 的 API 升级和 MySQL 驱动 5.x 到 8.x 的版本升级是两条独立的线,Bridge 让它们互不干扰。
消息通知系统适合 Bridge 是因为:消息类型和发送渠道都是业务驱动的高频变化点。今天加一种 Push 消息,明天换一个华为云渠道,两条线各自迭代。
如果两个维度中有一个几乎不变,Bridge 就是杀鸡用牛刀。别为了设计模式而设计模式。
Bridge 的本质就一句话:用组合代替继承,让两个维度各自扩展。 JDBC 靠这个设计跑了 30 年没被重构,不是因为 Bridge 多精妙,是因为它解决了真实的多维度扩展问题。
顺便说一句,我在做的那个小程序「爪爪代码冒险记」里,Bridge 模式是用卡皮巴拉过河找桥来讲的------河两边各自独立,中间架一座桥才能通行。比直接讲 JDBC 好理解一点,感兴趣可以搜搜看。