结构型模式的最后一个,是桥接模式(Bridge) 。它可能是七个结构型里最抽象、也最容易被讲得云里雾里的一个,但只要抓住它要解决的那个具体问题,就会豁然开朗。这个问题是:当一个东西同时在两个(或多个)独立的维度上变化时,如果用继承去表达,类的数量会"维度相乘"式爆炸。
我们在装饰器那篇见过一次类爆炸(2ⁿ 组合),但那是"同一维度的多个可选增强"的叠加。桥接面对的是另一种类爆炸,更常见也更隐蔽------两个正交维度的交叉 。举个我们订单场景里的例子:订单有不同类型 (普通订单、团购订单、预售订单),发通知又有不同渠道 (短信、App 推送、邮件)。这是两个完全独立的维度:订单类型的增减,和通知渠道的增减,毫不相干。可如果你用继承硬来,就会写出 普通订单短信通知、普通订单App通知、团购订单邮件通知...... 3 种类型 × 3 种渠道 = 9 个类,再加一种类型或渠道,就是 12 个、16 个,呈乘法增长。
桥接模式的核心思想,就是把这两个纠缠在一起的维度拆开 ,让它们各自独立地变化,再用一座"桥"把它们连起来。GoF 给它的正式定义是一句很经典但初看费解的话:"将抽象部分与它的实现部分分离,使它们都可以独立地变化。" 这一篇我们就把这句话彻底翻译成人话。
这篇文章按这条线索展开:先复现"两个维度用继承"导致的乘法类爆炸;再引出桥接如何把维度拆开、用组合搭一座桥;然后讲清那句"抽象与实现分离"到底在说什么(这是全篇最关键、也最容易误解的地方);接着看它的现实身影;最后辨析它与相近模式,给出适用边界,并为结构型模式画上句号。贯穿例是订单类型 × 通知渠道。
目录
- 两个维度用继承:乘法式类爆炸
- 桥接模式:把维度拆开,搭一座桥
- "抽象与实现分离"到底在说什么
- [现实身影:JDBC 与日志框架](#现实身影:JDBC 与日志框架)
- [桥接 vs 相近模式,以及什么时候用](#桥接 vs 相近模式,以及什么时候用)
- 结构型模式,到此收官
一、两个维度用继承:乘法式类爆炸
先把问题摆清楚。我们要做"给订单发通知"这件事,它牵涉两个维度:
- 维度 A:订单类型 ------ 普通订单、团购订单、预售订单(不同类型通知的文案、逻辑不同);
- 维度 B:通知渠道 ------ 短信、App 推送、邮件(不同渠道的发送方式不同)。
如果用继承来表达"某类订单通过某渠道发通知",你会不自觉地写成这样:
java
abstract class OrderNotify { abstract void notifyUser(); }
// 为"类型 × 渠道"的每一种组合,都建一个类
class NormalOrderSmsNotify extends OrderNotify { ... } // 普通订单 + 短信
class NormalOrderAppNotify extends OrderNotify { ... } // 普通订单 + App
class NormalOrderEmailNotify extends OrderNotify { ... } // 普通订单 + 邮件
class GroupOrderSmsNotify extends OrderNotify { ... } // 团购订单 + 短信
class GroupOrderAppNotify extends OrderNotify { ... } // 团购订单 + App
// ... 3 类型 × 3 渠道 = 9 个类
灾难在于乘法增长 :现在是 3×3=9 个类。产品说"再加一个预售+的订单类型",你要加 3 个类(配齐三个渠道);运营说"接入一个企业微信渠道",你要加 3 个类(配齐三种订单)。每增加一个维度上的一项,类的数量就按另一个维度的规模成倍增加。 而且这些类里全是重复------NormalOrderSmsNotify 和 GroupOrderSmsNotify 里,"发短信"的代码几乎一样,只是订单文案不同。
问题的根源是:继承只有一条轴,却被迫同时表达两个维度。 你把"订单类型"和"通知渠道"这两件本来毫不相干的事,硬塞进了同一条继承链,于是它们被死死绑在一起,任何一个变,都牵动另一个。
正确的直觉应该是:这俩维度本来就该分开。 "订单类型"该有自己的一套类,"通知渠道"该有自己的一套类,然后在运行时把"某个类型"和"某个渠道"自由组合 起来------3 个类型类 + 3 个渠道类 = 6 个类 ,就能覆盖 9 种组合;以后加一个类型是 +1,加一个渠道也是 +1,变回了加法。怎么做到?用组合,搭一座桥。
二、桥接模式:把维度拆开,搭一座桥
桥接的做法:把两个维度拆成两套独立的继承体系,然后让其中一个体系(抽象)持有另一个体系(实现)的引用------这个"持有"的组合关系,就是那座"桥"。
先把"通知渠道"这个维度独立出来,做成一套体系(桥接里管它叫"实现"部分):
java
// 维度 B:通知渠道 ------ 独立的一套体系
public interface NotifyChannel {
void send(String message);
}
class SmsChannel implements NotifyChannel { public void send(String m){ /* 发短信 */ } }
class AppChannel implements NotifyChannel { public void send(String m){ /* App 推送 */ } }
class EmailChannel implements NotifyChannel { public void send(String m){ /* 发邮件 */ } }
再把"订单类型"这个维度也独立出来(桥接里管它叫"抽象"部分),关键是------它持有一个 NotifyChannel 引用,这就是桥:
java
// 维度 A:订单类型 ------ 另一套体系,持有渠道引用(这就是"桥")
public abstract class OrderNotify {
protected final NotifyChannel channel; // ← 桥:持有另一个维度
public OrderNotify(NotifyChannel channel) {
this.channel = channel;
}
public abstract void notifyUser();
}
class NormalOrderNotify extends OrderNotify {
public NormalOrderNotify(NotifyChannel channel) { super(channel); }
public void notifyUser() {
channel.send("您的订单已创建"); // 委托给渠道去发,自己只管文案
}
}
class GroupOrderNotify extends OrderNotify {
public GroupOrderNotify(NotifyChannel channel) { super(channel); }
public void notifyUser() {
channel.send("拼团成功,订单已生成"); // 团购的文案不同,但发送委托给渠道
}
}
现在,"某类订单 + 某渠道"的组合,是在运行时自由拼出来的:
java
// 普通订单 + 短信
OrderNotify n1 = new NormalOrderNotify(new SmsChannel());
n1.notifyUser();
// 团购订单 + 邮件 ------ 随意组合,不用新建类
OrderNotify n2 = new GroupOrderNotify(new EmailChannel());
n2.notifyUser();
对比第一节,升级点非常清晰:类的数量从 3×3=9 个,降到了 3+3=6 个;从乘法变回了加法。 加一个订单类型?加一个 OrderNotify 子类(+1)。加一个通知渠道?加一个 NotifyChannel 实现(+1)。两个维度各自独立地变化,谁也不牵连谁------这正是 GoF 定义里"都可以独立地变化"的含义。用一张图看这座"桥"最直观:

图里最该记住的,就是中间那条连接两套体系的"桥"(OrderNotify 持有 NotifyChannel)。左右两栏可以各自向下无限扩展,而它们的组合数是"左 × 右",类的数量却只是"左 + 右"。桥接的全部威力,就浓缩在这座用组合搭起来的桥上------它又一次是"组合优于继承"的胜利。
三、"抽象与实现分离"到底在说什么
现在来啃 GoF 那句定义:"将抽象 部分与它的实现 部分分离。" 这句话之所以让无数人困惑,是因为这里的"抽象"和"实现",不是我们平时说的"抽象类/接口"和"实现类"的意思。必须把它翻译清楚,否则你会一直误解桥接。
在桥接的语境里:
- "抽象"(Abstraction) 指的是主体维度、高层的那套逻辑 ------ 在我们的例子里就是
OrderNotify(订单类型)。它是面向用户、定义"要做什么"的那一侧。 - "实现"(Implementor) 指的是被主体维度调用的、底层的那套能力 ------ 就是
NotifyChannel(通知渠道)。它提供"具体怎么做"的基础操作,供上层调用。
所以"抽象与实现分离",翻译成人话就是:把"高层逻辑维度"和"底层能力维度"拆成两套独立的体系,让高层通过组合去调用底层,而不是用继承把两者焊死。 OrderNotify(高层)通过持有的 channel(桥)去调用 NotifyChannel(底层)的能力,两者解耦,各自能独立扩展和替换。
这里有个关键点,也是桥接和策略模式(下一篇)容易混的地方:桥接强调的是两侧都会独立演化的、稳定的维度划分 ------它是一种架构层面的结构设计 ,你在设计之初就预见到"这里有两个正交维度会各自增长",于是提前用桥把它们分开。它不是临时替换一个算法,而是让两个类体系长期地、各自地生长。理解了"抽象=主体维度、实现=被调用的能力维度",桥接就不再神秘了------它就是把'本该分开的两个维度'用组合连起来,如此而已。
四、现实身影:JDBC 与日志框架
桥接最经典的工业级案例,是 JDBC。想想 JDBC 的结构:
- 抽象侧 :
java.sql包里的Connection、Statement、DriverManager这套 API,是面向应用开发者的、稳定的"高层抽象"------你写的代码只调这套接口; - 实现侧 :各家数据库厂商提供的 JDBC 驱动(MySQL 的、Oracle 的、PostgreSQL 的),是"底层实现"------它们各自实现了 JDBC 定义的接口。
你的应用代码(抽象侧)通过 DriverManager 持有一个具体的 Driver(实现侧),这就是那座桥。于是两侧独立变化 :JDK 升级 JDBC API(抽象侧演进)不影响驱动;数据库厂商更新驱动(实现侧演进)不影响你的应用代码;你想从 MySQL 换成 Oracle,只需换个驱动(换实现),应用代码一行不改。这正是桥接"抽象与实现各自独立变化"的完美体现,也是为什么 JDBC 能让一套代码适配几十种数据库。
另一个例子是 日志框架的门面 + 实现:SLF4J(抽象侧,统一 API)+ Logback/Log4j2(实现侧,具体日志实现)。你的代码面向 SLF4J,底层实现可随意替换。
有意思的是,SLF4J 我们在外观那篇也提过。这说明一个真实的框架,往往同时用到多个模式:SLF4J 对使用者是"外观"(简化日志的使用),对"API 与实现的解耦"则体现了"桥接"的思想。模式不是互斥的标签,而是从不同角度描述同一个设计的多个侧面------不必纠结"它到底是哪个模式",理解它解决了什么问题更重要。
五、桥接 vs 相近模式,以及什么时候用
桥接容易和几个模式混,辨析一下:
- 桥接 vs 策略(下一篇) :两者代码结构很像(都是持有一个接口引用、委托调用)。区别在意图和粒度 :策略是"一个算法有多种可替换的实现 ",关注的是运行时替换单一行为 ,通常只有"一个"变化维度(策略本身);桥接是"两个维度都会独立、长期地扩展 ",关注的是架构层面的维度分离 ,强调的是"两侧各自成体系地生长"。一句话:策略换的是"一个算法",桥接分的是"两条演化轴"。
- 桥接 vs 适配器 :适配器是事后补救 ------两个已经存在、接口不兼容的东西,加个转接头让它们能协作;桥接是事前设计------在设计之初就主动把两个维度分开,让它们从一开始就能独立生长。适配器解决"已经不兼容了怎么办",桥接解决"怎么设计才不会耦合"。
适合用桥接的信号:
- 一个类存在两个或多个独立变化的维度(正交),且每个维度都可能扩展;
- 你不希望这些维度之间用继承产生"乘法式"的类爆炸;
- 你想在运行时灵活组合不同维度的实现。
不必用的信号:
- 只有一个变化维度------那可能用策略、或者别的模式就够了,桥接是为"多维度"准备的;
- 两个维度中有一个其实是稳定的、几乎不变的------那分离的收益不大,可能是过度设计。
判断的核心还是那句话:先确认真的存在"两个都会独立增长的维度",桥接才有价值。 很多时候你以为有两个维度,其实一个维度是固定的,那就别为想象中的第二维度提前搭桥------这又是过度设计的陷阱。
六、结构型模式,到此收官
第 13 篇结束,七个结构型模式全部讲完了。它们关心"类和对象如何组合连接",我们用一张总表收尾,把整个家族刻进脑子:
| 模式 | 一句话 | 核心意图 | 关键手法 |
|---|---|---|---|
| 代理 Proxy | 给对象一个替身 | 控制访问 | 同接口包装 + 织入 |
| 装饰器 Decorator | 洋葱层层包裹 | 动态增强功能 | 同接口 + 组合 + 递归 |
| 适配器 Adapter | 接口转接头 | 转换接口 | 转换 + 委托 |
| 组合 Composite | 单个和一组一样 | 统一处理树形 | 容器持有抽象组件 |
| 外观 Facade | 给子系统盖门面 | 简化使用 | 中间层收拢编排 |
| 享元 Flyweight | 提取共享省内存 | 扛住海量对象 | 内外状态分离 + 缓存池 |
| 桥接 Bridge | 分离两个维度 | 让维度独立变化 | 抽象持有实现(搭桥) |
回望这七个模式,一条主线贯穿始终:它们几乎全都在"用组合代替继承"。 代理、装饰、适配、桥接靠组合持有对象,组合靠组合搭树,外观靠组合收拢子系统,享元靠组合(引用)共享。第一篇讲的"合成复用原则(组合优于继承)",在整个结构型家族里被反复验证------当你想改变、扩展、连接对象的行为时,组合几乎总比继承更灵活。这也是结构型模式给我们最深的一课。
小结 。桥接模式专治"两个独立维度用继承导致的乘法类爆炸"。它把两个维度拆成两套独立体系,让"抽象侧"(主体维度/高层逻辑)通过组合持有"实现侧"(被调用的能力维度)的引用------这个组合关系就是"桥",从而让两个维度各自独立地变化 ,把类的数量从"乘法"变回"加法"。GoF 那句"抽象与实现分离"的真正含义,是"把高层逻辑维度和底层能力维度解耦",JDBC 的 API 与驱动、SLF4J 与日志实现都是它的经典身影。至此,结构型七模式全部收官,而它们共同的底色,就是"组合优于继承"。从下一篇开始,我们进入设计模式里数量最多、也最精彩的一类------行为型模式 ,它们关心"对象之间如何协作、如何分配职责"。第一站是消灭 if-else 的利器:策略模式。