桥接不包层、也不动接口,而是把一棵继承树剪成两棵,让「抽象」和「实现」各自独立地变化。它也是结构型里最容易「以为懂了、其实用错了」的一个:结构几乎和策略模式一模一样,意图却完全不同。本文讲清四个角色与那条「桥」到底桥在哪、判断两个维度是否真正交的 3 条自检、桥接的三条边界,拆开 JDBC 这个教科书级案例,最后把桥接、策略、适配器三者的界限一次讲透。
一、问题的起点:两个维度一起变,继承会「乘积级」爆炸
先看一个再普通不过的场景:消息通知。
- 消息有几种类型:普通消息、加急消息、定时消息;
- 消息有几种发送渠道:站内信、短信、邮件。
这是两个各自独立的方向:产品会加新类型(比如「审批提醒」),运维会加新渠道(比如「企业微信」)。如果直接用继承把它们揉在一起:
class NormalSmsMessage extends Message { } // 普通 + 短信
class NormalEmailMessage extends Message { } // 普通 + 邮件
class NormalInAppMessage extends Message { } // 普通 + 站内信
class UrgentSmsMessage extends Message { } // 加急 + 短信
// ... 还有 5 个,一共 3 × 3 = 9 个子类
类数 = 类型数 × 渠道数。这个乘法是问题的核心:
| 变更 | 继承方案 | 桥接方案 |
|---|---|---|
| 初始 3 类型 × 3 渠道 | 9 个类 | 3 + 3 = 6 个类 |
| 新增 1 个渠道 | +3 个类(每种类型各加一个子类) | +1 个类,不动已有代码 |
| 新增 1 个类型 | +3 个类 | +1 个类 |
| 变更波及面 | 改一个渠道要动 3 处 | 只动 1 处 |
所以桥接要解决的问题不是「少写几个类」,而是修改的波及面:继承方案里,加一个渠道会横向牵动所有类型;桥接方案里,两个维度各自生长、互不打扰。这就是开闭原则在结构层面的落地。

二、结构:四个角色,一条「桥」
桥接的结构图里会出现两棵继承树,中间用一条组合关系连起来——那条组合关系就是「桥」:

- Abstraction(抽象化):
Message。定义抽象层的接口,并持有一个 Implementor 的引用——这个字段就是桥。它不再自己动手干活,而是把活交给桥对面的实现。 - RefinedAbstraction(修正抽象化):
NormalMessage、UrgentMessage。在抽象层扩展(加急 = 前缀 + 连发三次,这就是类型维度的差异)。 - Implementor(实现化):
MessageSender。实现部分的接口,只管「怎么发出去」。 - ConcreteImplementor(具体实现化):
SmsSender、EmailSender。具体渠道的落地。
关于「桥」这个名字:这是因为抽象层不自己干活,而是把活交给实现层——中间那条「持有一个实现」的组合关系,就是让两边独立生长的桥。桥这头是「消息是什么类型」,桥那头是「消息怎么发出去」;两边可以各自施工、各自加宽,而不用把整座桥拆了重铺。
两个容易忽略的点:
- Implementor 的接口通常比 Abstraction 的接口更「底层」。
Message.send(target)是业务语言,MessageSender.send(target, content)是通道语言;抽象层负责把业务语言翻译成通道语言。这一点是它和策略模式最微妙的差别。 - 抽象侧的继承和实现侧的继承是独立的两棵树,桥只负责把它们连起来。所以两类变更各自局部化:加类型改左树,加渠道改右树。
三、手写一条完整的桥
下面是可直接运行的单文件版本(Java 8+):
/** Implementor:实现部分的接口——只回答「怎么发」,不关心「发什么类型的消息」 */
interface MessageSender {
void send(String target, String content);
}
class SmsSender implements MessageSender {
@Override
public void send(String target, String content) {
System.out.println("[短信 → " + target + "] " + content);
}
}
class EmailSender implements MessageSender {
@Override
public void send(String target, String content) {
System.out.println("[邮件 → " + target + "] " + content);
}
}
/** Abstraction:抽象部分——持有实现部分的引用,这就是「桥」 */
abstract class Message {
protected final MessageSender sender; // 桥
protected Message(MessageSender sender) {
this.sender = sender;
}
/** 抽象层定义「发消息」这件事,具体怎么发交给桥对面的实现 */
public abstract void send(String target);
}
/** RefinedAbstraction:修正抽象化——在抽象层做扩展 */
class NormalMessage extends Message {
NormalMessage(MessageSender sender) {
super(sender);
}
@Override
public void send(String target) {
sender.send(target, "您有一条新消息");
}
}
/** RefinedAbstraction:加急消息 = 加前缀 + 连发三次 */
class UrgentMessage extends Message {
UrgentMessage(MessageSender sender) {
super(sender);
}
@Override
public void send(String target) {
for (int i = 1; i <= 3; i++) {
sender.send(target, "【加急】请立即处理(第 " + i + " 次)");
}
}
}
public class BridgeDemo {
public static void main(String[] args) {
// 两个维度自由组合:2 种类型 × 2 种渠道,只用了 4 个具体类
Message m1 = new NormalMessage(new SmsSender());
Message m2 = new UrgentMessage(new EmailSender());
// 渠道还能在运行时替换(用 lambda 临时实现一个渠道),抽象层代码一行不用改
// 注意:lambda 的参数必须与 MessageSender.send(String target, String content) 对齐
Message m3 = new NormalMessage(
(target, content) -> System.out.println("[内网 → " + target + "] " + content));
m1.send("138****8000");
m2.send("ops@example.com");
m3.send("user-42");
}
}
输出:
[短信 → 138****8000] 您有一条新消息
[邮件 → ops@example.com] 【加急】请立即处理(第 1 次)
[邮件 → ops@example.com] 【加急】请立即处理(第 2 次)
[邮件 → ops@example.com] 【加急】请立即处理(第 3 次)
[内网 → user-42] 您有一条新消息
三点值得注意:
MessageSender是函数式接口(唯一抽象方法send(String target, String content)),所以最后一个例子能直接写 lambda 临时实现一个渠道——但要注意 lambda 的参数必须与这个方法对齐:写成(target, content) -> ...才对,只写一个target -> ...是编译不过的。这个技巧在写测试和 mock 时非常好用。- 加急的「连发三次」写在抽象层(
UrgentMessage),而「怎么发出去」写在实现层(SmsSender)——职责没有交叉,这正是桥接的收益。 m3把渠道换成了 lambda,Message的代码一个字都没改。桥接让「换实现」退化成「注入不同对象」。
一次 UrgentMessage.send() 的调用,实际是这样过桥的:

抽象层负责业务语义(加急该怎么办),实现层负责通道细节(短信怎么发)。任何一侧改动,都不会顺着桥传到对岸——这就是「解耦」在代码里的具体样子。
四、桥接最重要的语义:正交
桥接成立的前提只有一个词:正交——两个维度可以自由组合、互不约束。落地前先用这 3 条自检:
| # | 自检问题 | 不通过意味着什么 |
|---|---|---|
| 1 | 两个维度是否都会独立变化? | 只有一个会变 → 你要的是策略模式,不是桥接 |
| 2 | 是否任意组合都成立? | 某些组合天生无意义(如「定时 × 站内信不支持」)→ 维度间有约束,拆开后这份约束就没人管了 |
| 3 | 抽象层的方法是否不需要知道实现细节? | 需要 → 两个维度没真正解耦,桥是假的 |
第 2 条最容易被忽略。举几个对比:
- 成立:消息类型 × 发送渠道——每一种组合都有意义;
- 成立:报表数据源(订单/用户/财务)× 导出格式(Excel/CSV/PDF);
- 不成立:支付方式 × 业务场景——「货到付款」天然不支持跨境场景,硬拆就得靠一堆
if把约束塞回来。
还有一点反直觉但很实用:桥接让「换实现」变成注入。
// 构造注入(final):对象生命周期内实现不变,最稳,优先选它
class NormalMessage extends Message {
NormalMessage(MessageSender sender) { super(sender); }
}
// Setter 注入:运行时可换渠道,灵活,但引入了可变状态
//(用这个变体时,基类 Message 里的 sender 要去掉 final)
class MutableMessage extends Message {
MutableMessage(MessageSender sender) { super(sender); }
public void setSender(MessageSender sender) {
this.sender = sender;
}
}
可变性也是一种设计决策:不需要运行时切换,就用构造注入(final 字段),别默认给出 Setter。
正交带来的收益,在「需求持续加码」时才看得最清楚——两个维度各自生长,互不影响:

新增「定时消息」只动左树(+1 个类),新增「企业微信」只动右树(+1 个类);而且两侧新增的类都能和对面已有的任意类型自由配对,不需要再补 M 个子类。这才是桥接真正的收益。
五、桥接的三条边界
- 只有一个维度在变 → 不要搭桥。变化的是「算法」就用策略,桥接会多出一层没必要的间接。
- 两个维度之间有强约束 → 不要硬拆。拆开之后约束无处安放,最终会以散落的
if形式回到代码里,比不拆更糟。 - 维度还没定型 → 先别搭桥。桥接是把「变化点」显式建模成两棵树;如果连哪两个维度会变都还没想清楚,搭出来的桥大概率是要废弃的。桥接是事前设计,不是事后补救。
六、JDBC 就是桥接(教科书级案例)
Java 里最经典的桥接不是书上的例子,而是你每天都在用的 JDBC:
- 抽象部分:
java.sql那一套——Connection、Statement、ResultSet; - 实现部分:各数据库厂商的驱动(MySQL 的
com.mysql.cj.jdbc.*、PostgreSQL 的org.postgresql.*); - 桥:
DriverManager.getConnection()把请求转给已注册的具体Driver实现。
好处非常直观:业务代码只依赖 java.sql。从 MySQL 换到 PostgreSQL,改动是「换一个 jar 包 + 改连接串」,业务代码一行不动——两个维度(SQL 使用方式 / 数据库厂商)各自演化,互不牵动。

(上图为简化示意:真实 JDK 中 DriverManager 通过注册的 Driver 实例拿到连接,厂商的 Connection 实现由驱动 jar 提供。)
// 业务代码只依赖 java.sql ——从 MySQL 换到 PostgreSQL,这一整段都不用动
try (Connection conn = DriverManager.getConnection(url, user, pwd);
PreparedStatement ps = conn.prepareStatement("select * from dorm_room where id = ?")) {
ps.setLong(1, 101L);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("room_no"));
}
}
}
其他真实例子:
- AWT 的 Peer 架构:
java.awt.Component(抽象层)与java.awt.peer.ComponentPeer(实现层,各平台原生组件)分离,早期 Java 的跨平台 GUI 就靠这座桥。 - SLF4J 与具体日志实现:
slf4j-api(抽象)× logback / log4j2(实现),应用只依赖 API,换日志实现只换绑定。严格说它更常被称为「门面 + 绑定」,但它确实让两个维度独立演化——这正是桥接的思路。
七、桥接 vs 策略 vs 适配器
这三个(加装饰者)是结构型里最常被搞混的一组。它们的结构都是「组合」,差别在意图和演化方向:
| 桥接 Bridge | 策略 Strategy | 适配器 Adapter | |
|---|---|---|---|
| 意图 | 分离两个独立变化的维度 | 替换算法/行为 | 转换不匹配的接口 |
| 继承树数量 | 两棵(抽象侧 + 实现侧) | 一棵(策略实现) | 一棵(被适配者的体系) |
| 抽象层会不会继续长出子类 | 会(NormalMessage/UrgentMessage) | 一般不会 | 不会 |
| 两侧接口的关系 | 抽象层方法组合实现层的底层操作 | 接口签名与上下文基本一致 | 目标接口由调用方规定 |
| 使用时机 | 事前设计 | 运行时可切换 | 事后兼容既有代码 |
一句话记忆:
- 桥接:抽象层自己还会长新分支,实现层也会长新分支 → 两棵树。
- 策略:上下文不怎么变,变的是「换哪种算法」→ 一棵树。
- 适配器:接口已经对不上了,来补一块 → 事后补救,通常只有一棵树。

八、常见误区与踩坑
- 把策略当桥接:只有算法在变、上下文稳定,那就是策略。判断标准是「抽象侧是否会长出新分支」,而不是「用了组合」。
- 两个维度没正交就硬拆:拆完约束丢失,用一堆
if补回来,代码比不拆更复杂。 - Implementor 设计成 Abstraction 的复制品:如果
MessageSender.send()的签名和Message.send()一模一样,那实现层没提供任何东西,桥白搭——实现层要提供更基础的能力。 - 无脑给 Setter:运行时切换实现很爽,但会引入「对象在使用过程中被改掉」的不确定性。不需要切换就用
final构造注入。 - 过度前瞻:为一个「可能永远只有一个实现」的维度搭桥,是典型的过度设计。桥接要在第二个实现出现时再补,而不是一开始就搭。
评论