桥接模式设计思想
# 第三卷第10章:桥接模式设计思想
📚 本篇渐进学习节奏(建议按顺序食用)
本篇采用「事故复盘 → 失败探索 → 模式诞生 → 实现对比 → 效果验证 → 反面踩坑 → 选型决策」的节奏:
- 第 01 节 · 案例引入 — 多端推送系统:4端×4通道→扩张到5端×7通道,35个子类爆炸事故
- 第 02 节 · 失败探索 — 纯继承 / if-else判断 / 两层继承树,三次直觉方案全部翻车
- 第 03 节 · 模式基础 — 从"乘法变加法"讲透两个独立维度的解耦
- 第 04 节 · 实现对比 — 经典四角色 / SPI桥接 / 轻量组合 / 接口内部类四种实现
- 第 05 节 · 效果对比 — 35个子类 vs 12个类,改算法 1 处 vs 35 处,数据说话
- 第 06 节 · 反面踩坑 — 4 种翻车姿势:非正交维拆分 / 抽象层退化成空壳 / 客户端写死组合 / 桥接适配混淆
- 第 07 节 · 决策树 — 工程师的成熟度在于"知道什么时候不上桥接"
- 第 08 节 · 总结延伸 — 思考模型沉淀 + 与适配器/策略/装饰者/抽象工厂的边界
阅读到任一节卡壳,直接跳回上一节复盘场景;本篇代码均可直接运行。
# 推荐一个好玩网站
一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网 (opens new window)
https://yccoding.com/
# 目录快速导航
# 01.案例引入:推送系统的"笛卡尔积"爆炸事故
本篇主线:两个独立变化的维度被继承绑死 → 引出"把乘法变成加法"的桥接思想。
# 1.1 痛点现场
🔥 模拟事故复盘 · 消息推送中台 · 国际化扩张时的"维度灾难"
9 月 12 日上午 10:00,市场部宣布:"Q4 公司要拓展 3 个海外区域,App 端先支持东南亚、欧美、日韩本地化推送渠道。" 后端组打开当前推送系统一看——当前实现是用『继承』把"端 × 通道"绑死的:4 个端(PC / iOS / Android / Web)× 4 个通道(微信 / 钉钉 / 邮件 / SMS)= 16 个子类:
PCWeChatPusher、IOSDingTalkPusher、AndroidEmailPusher、WebSMSPusher... 现在要新增 3 个海外通道(WhatsApp / Line / Telegram),按现有写法:
- 新增类数 = 4 端 × 3 新通道 = 12 个新子类;
- 新人小赵手抖,把
IOSWhatsAppPusher的"重试逻辑"复制粘贴时漏掉一行——iOS 推送失败后没退避就疯狂重试,WhatsApp 接口被风控限流,全公司 iOS 用户当晚收不到推送。更糟的是 9 月 20 日运营追加:"新增『车机端』,因为我们要进车厂。"——4 个新通道还没加完,又来了一个新端:
- 新增类数 = 1 新端 × 7 通道(4 老 + 3 新)= 7 个新子类;
- 总子类数从 16 → 16 + 12 + 7 = 35 个;
- 改一个"重试退避算法"——35 个类要全改一遍;
- 单测脚本 35 套要重写。
这场事故暴露了一个本质问题:当类的"变化维度"是两个或更多时(端 × 通道),用继承会让类数量按笛卡尔积爆炸。每加一个端 → 类数 ×(通道数);每加一个通道 → 类数 ×(端数)。
做一套智能家居控制:遥控器有 BasicRemote / AdvancedRemote 两种,设备有 TV / AirConditioner / Light 三种。第一版用继承:
class BasicRemoteForTV { ... }
class BasicRemoteForAC { ... }
class BasicRemoteForLight { ... }
class AdvancedRemoteForTV { ... }
class AdvancedRemoteForAC { ... }
class AdvancedRemoteForLight { ... }
// 2 × 3 = 6 个类
2
3
4
5
6
7
一维增加时成本是线性的,但双维度相乘后是笛卡尔积爆炸:
flowchart TD
subgraph Remote维度
B[Basic]
A[Advanced]
end
subgraph Device维度
TV
AC
L[Light]
end
B --> BT[BasicRemoteForTV]
B --> BA[BasicRemoteForAC]
B --> BL[BasicRemoteForLight]
A --> AT[AdvancedRemoteForTV]
A --> AA[AdvancedRemoteForAC]
A --> AL[AdvancedRemoteForLight]
style BT fill:#fee
style BA fill:#fee
style BL fill:#fee
style AT fill:#fee
style AA fill:#fee
style AL fill:#fee
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
新增一个"语音遥控器" + 一个"窗帘设备":立刻从 6 个类变成 4 × 4 = 16 个。
# 1.2 直觉实现复现
【你也能写出这种代码】。一个新同学接手需求"iOS 端接入 WhatsApp 推送",第一反应是照着现有 IOSWeChatPusher 复制一个:
// ❌ 事故现场——照着 IOSWeChatPusher 复制粘贴出 IOSWhatsAppPusher
class IOSWeChatPusher extends BasePusher {
public void push(Message msg) {
initConnection(); // ① iOS 推送初始化
applyRetryPolicy(); // ② 退避重试(iOS 独有的指数退避算法)
WeChatChannel.send(msg); // ③ 调用微信通道
}
private void applyRetryPolicy() {
// 指数退避: 1s → 2s → 4s → 8s
}
}
// 新人复制上面,改成 WhatsApp
class IOSWhatsAppPusher extends BasePusher {
public void push(Message msg) {
initConnection();
// ❌ applyRetryPolicy(); ← 漏掉了!WhatsApp 没重试,失败直接报错
WhatsAppChannel.send(msg);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
🧪 跑一下,亲眼看到 bug
// 模拟:WhatsApp 通道间歇性超时
IOSWhatsAppPusher pusher = new IOSWhatsAppPusher();
for (int i = 0; i < 10; i++) {
pusher.push(new Message("test"));
// 第 3 条超时 → 没重试 → 直接抛异常 → 用户收不到推送
// 第 5 条超时 → 疯狂调用 send() 没退避 → WhatsApp 风控限流
}
// 结果:全公司 iOS 用户当晚收不到推送,WhatsApp 接口被限流 24 小时
2
3
4
5
6
7
8
事故现场重现完毕——35 个类中任何一处漏复制一行逻辑 = 线上灾难。
💭 3 个反思题(先别往下看,自己想 30 秒):
- 如果"端"从 4 种变成 10 种,"通道"从 4 种变成 15 种,子类数量是多少?
- 改"退避算法"要从多少个类里改?
- 有没有一种方式,让"端"和"通道"各自独立发展,加新端不影响通道,加新通道不影响端?
# 1.3 问题根源拆解
【画一张图就清楚了】
flowchart LR
subgraph 笛卡尔积爆炸[继承体系:端 × 通道 = 笛卡尔积]
P[BasePusher] --> PC[PCWeChat]
P --> PI[IOSWeChat]
P --> PA[AndroidWeChat]
P --> PW[WebWeChat]
P --> PCD[PCDingTalk]
P --> PID[IOSDingTalk]
dots[... 16 个 → 要扩到 35 个]
end
style 笛卡尔积爆炸 fill:#ffe6e6
style dots fill:#fee
2
3
4
5
6
7
8
9
10
11
12
每个"端 × 通道"子类 各自维护推送逻辑和通道调用,互不感知,这就埋下了 5 类隐患:
| 隐患 | 现象 | 业务影响 |
|---|---|---|
| 笛卡尔积爆炸 | m × n 个子类,任一维扩展 = 乘法增长 | 5端×7通道 = 35个类,每加一维翻倍 |
| 代码重复 | 16 个类各写一份 iOS 退避逻辑 | 改退避算法 → 改 16 处 |
| 维度耦合 | 端和通道绑死在同一继承树 | 改通道协议 → 所有端都受影响 |
| 无法运行时切换 | 编译期类型固定 | iOS 不能从微信切到 WhatsApp 推送 |
| 测试爆炸 | 35 个组合都要覆盖 | 退避回归 → 跑 35 套用例 |
🎯 核心矛盾:业务上"端"和"通道"是两个独立变化维度(加新端 ≠ 加新通道),但代码层面用继承把两个维度硬绑在一起,导致"乘法级"的组合爆炸。
# 1.4 引出本篇主角
桥接模式(Bridge)的核心思想:把"两个独立变化的维度"拆开,一个作为抽象层(遥控器/端),另一个作为实现层(设备/通道),中间用组合连接。两边可以各自扩展,互不影响。
flowchart LR
subgraph 抽象层[抽象层:推送器]
R[Pusher<br/>持有 Channel]
R --> BR[IOSPusher]
R --> AR[AndroidPusher]
end
subgraph 实现层[实现层:通道]
D[Channel 接口]
D --> WC[WeChat]
D --> WA[WhatsApp]
D --> LN[Line]
end
BR -.组合桥.-> D
AR -.组合桥.-> D
style R fill:#e6f3ff
style D fill:#fdf6e3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
扩展成本立刻从"乘法"退化成"加法":
flowchart LR
subgraph 继承[继承方案]
C1[4x4 = 16 类<br/>扩到5x7 = 35 类]
end
subgraph 桥接[桥接方案]
B1[4+4 = 8 类<br/>扩到5+7 = 12 类]
end
style C1 fill:#fee
style B1 fill:#dfd
2
3
4
5
6
7
8
9
JDBC 的 DriverManager(抽象)× Driver 实现(MySQL/Oracle/PG)、SLF4J 日志门面(抽象)× 实现(Log4j/Logback),都是桥接的经典落地。
但是!先别急着看实现。下一节,我们先看看新手通常会先尝试哪些"看起来很合理"的方案,并理解它们为什么都不够好。
# 02.三次失败探索
为什么要学这一节:直接给你"标准答案"是容易的,但桥接模式不是凭空发明的——它是前人走过三条死路之后才提炼出来的。走过这些死路,你才会真正理解为什么代码长那个样子。
# 2.1 尝试方案A:纯继承——笛卡尔积爆炸
【新手方案①:每个"端 × 通道"组合建一个子类】
// 方案A:纯继承实现——iOS 端的 4 个通道子类
class IOSWeChatPusher {
public void push(Message msg) {
initIOSConnection(); // iOS 特有逻辑(每个子类复制一遍)
applyRetryPolicy(); // 退避算法(每个子类复制一遍)
WeChatChannel.send(msg); // 微信通道调用
}
}
class IOSDingTalkPusher {
public void push(Message msg) {
initIOSConnection(); // 同上
applyRetryPolicy(); // 同上
DingTalkChannel.send(msg); // 只有这行不同
}
}
class IOSEmailPusher { /* 又复制一份 initIOSConnection + applyRetryPolicy */ }
class IOSSMSPusher { /* 又复制一份 */ }
// Android 端再复制 4 份、PC 端 4 份、Web 端 4 份 = 16 个类
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
🧪 跑一下,看会出什么问题
// 运营:新增"车机端"
// → 需要新增 CarWeChatPusher, CarDingTalkPusher, CarEmailPusher, CarSMSPusher
// → 4 个新类,每个要重新写车机端的推送逻辑
// 市场:新增 WhatsApp / Line / Telegram 三个海外通道
// → 5 端 × 3 新通道 = 15 个新类
// → 加上车机端的 4 个 = 总共新增 19 个类
// 运维:把退避算法从指数退避改成斐波那契退避
// → 35 个类全改
2
3
4
5
6
7
8
9
10
❌ 失败原因:类数量 = 端数 × 通道数,笛卡尔积;加一维 = 乘法增长;改一个通用逻辑 = 所有类全改。
💡 反思:我们需要把"端"和"通道"拆成两条独立的进化路线——加通道不动端,加端不动通道。
# 2.2 尝试方案B:if-else类型判断——修改扩散
【新手方案②:用一个方法 + int 标志位区分端和通道】
这是支付场景中最常见的反模式:更多内容 (opens new window)
public class PayController {
/**
* @param channelType 渠道类型 1 微信, 2 支付宝
* @param modeType 支付模式 1 密码, 2 人脸, 3 指纹
*/
public void doPay(String uId, String tradeId, BigDecimal amount,
int channelType, int modeType) {
// 微信支付
if (1 == channelType) {
System.out.println("微信渠道支付划账开始......");
if (1 == modeType) {
System.out.println("密码支付");
} else if (2 == modeType) {
System.out.println("人脸支付");
} else if (3 == modeType) {
System.out.println("指纹支付");
}
}
// 支付宝支付
if (2 == channelType) {
System.out.println("支付宝渠道支付划账开始......");
if (1 == modeType) {
System.out.println("密码支付");
} else if (2 == modeType) {
System.out.println("人脸支付");
} else if (3 == modeType) {
System.out.println("指纹支付");
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
🧪 跑一下,会发现隐藏问题
// 新增"京东支付"渠道 → 在 doPay 里加一个 if (3 == channelType) {} 块
// 新增"刷掌支付"模式 → 在 3 个渠道块里各加一个 else if (4 == modeType) {} 分支
// 改"密码支付"的风控逻辑 → 在 3 个渠道块里各改一遍
// 1 个方法 = 渠道数 × 模式数 个 if-else 分支
// 任何维度的扩展 = 修改同一处代码(违背开闭原则)
2
3
4
5
6
❌ 失败原因:① 一维扩展 → 必须修改 PayController 类(违背开闭原则);② 渠道和模式耦合在一个 if-else 森林里,任何改动都牵动全方法;③ 没有编译期类型安全——channelType=99 在运行时才报错。
💡 反思:我们需要渠道和模式的各自独立演化,每个新渠道是一个新类,每个新模式也是一个新类,然后用组合而非条件分支把它们连起来。
# 2.3 尝试方案C:两层继承树——无法表达任意组合
【新手方案③:先按"渠道"继承,再按"模式"继承】
// 第一层:按渠道分
abstract class Pay { abstract void doPay(); }
class WxPay extends Pay {
void doPay() { /* 微信划账逻辑 */ }
}
class AliPay extends Pay {
void doPay() { /* 支付宝划账逻辑 */ }
}
// 第二层:WxPay 下按模式再分
class WxPasswordPay extends WxPay {
void doPay() { super.doPay(); /* 加密码校验逻辑 */ }
}
class WxFacePay extends WxPay {
void doPay() { super.doPay(); /* 加刷脸校验逻辑 */ }
}
class WxFingerprintPay extends WxPay {
void doPay() { super.doPay(); /* 加指纹校验逻辑 */ }
}
// 支付宝下也要再分三层:
class AliPasswordPay extends AliPay { /* ... */ }
class AliFacePay extends AliPay { /* ... */ }
class AliFingerprintPay extends AliPay { /* ... */ }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
🧪 跑一下,看会怎样
// 新增"京东支付"渠道 → 第一层新增 JdPay 类
// 但问题:京东也支持密码/人脸/指纹三种模式
// → 必须在 JdPay 下再建 3 个子类,重复"密码校验"/"刷脸校验"/"指纹校验"逻辑
// 三层之下,密码逻辑在 WxPasswordPay、AliPasswordPay、JdPasswordPay 里各写一遍
// 改密码风控算法 → 改 3 处(未来 = 渠道数 处)
2
3
4
5
6
❌ 失败原因:① "先按渠道再按模式"的继承顺序把"模式"逻辑在每棵渠道子树下重复一遍;② 如果反过来"先按模式再按渠道",则渠道逻辑又在每个模式子树下重复;③ 本质上仍然是在某个维度上重复另一个维度的代码。
💡 反思:理想方案 = 方案 A 的"每个维度各有独立类" + 方案 B 的"运行时选择组合" + "两条维度通过组合连接,不通过继承捆绑"。
# 2.4 终于引出桥接模式
【三次失败之后,需求清单收敛了】
| 必须满足 | 来自哪一次失败 |
|---|---|
| ① 维 A 和维 B 各自独立扩展 | 2.1 纯继承——加通道要翻倍类数 |
| ② 不改现有代码即可加新维度成员 | 2.2 if-else——每次扩展改同一方法 |
| ③ 任意 A × 任意 B 都能组合 | 2.3 两层继承——固定了继承顺序 |
| ④ 通用逻辑只写一处 | 2.1 / 2.3——退避算法/风控逻辑到处复制 |
| ⑤ 运行时动态选择组合 | 2.2 if-else——编译期硬编码 |
【桥接模式的标准答案】——一套骨架,同时回答上面 5 条约束:
// ① 实现层——维 B(通道/模式),独立演化
public interface IChannel {
void send(Message msg);
}
// ② 抽象层——维 A(端/渠道),持有实现层引用
public abstract class Pusher {
protected IChannel channel; // ③ 组合桥接,非继承
public Pusher(IChannel channel) { this.channel = channel; }
public abstract void push(Message msg);
}
// ④ iOS 的通用退避逻辑,只在此一处
public class IOSPusher extends Pusher {
public IOSPusher(IChannel channel) { super(channel); }
public void push(Message msg) {
initIOSConnection();
applyRetryPolicy(); // ④ 只写一次
channel.send(msg); // ③ 组合调用实现层
}
}
// ② WhatsApp 通道独立存在
public class WhatsAppChannel implements IChannel {
public void send(Message msg) { /* WhatsApp 推送 */ }
}
// ⑤ 运行时任意组合:
Pusher pusher = new IOSPusher(new WhatsAppChannel()); // iOS + WhatsApp
pusher = new IOSPusher(new LineChannel()); // iOS + Line
pusher = new AndroidPusher(new TelegramChannel()); // Android + Telegram
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
短短几行,同时回答了上面 5 个需求。这就是桥接模式的"灵魂代码"。
# 03.桥接模式基础
# 3.1 从失败中提炼的需求
回顾 02 节,我们试了纯继承笛卡尔积、if-else类型判断、两层继承树——全部失败。现在拿着这些失败报告,问自己一个问题:
如果我要写一个能跑 3 年不崩的"双维度组合系统",它必须满足哪几条硬约束?
把这些约束写下来,就自然得到了桥接模式的设计清单:
| 约束 | 来自 | 代码体现 |
|---|---|---|
| ① 两条维度各自独立演化 | 2.1 笛卡尔积爆炸 | interface IChannel + abstract class Pusher 两条继承线 |
| ② 加新维度成员不碰现有代码 | 2.2 if-else 修改扩散 | 新增 TelegramChannel implements IChannel,不碰 Pusher 体系 |
| ③ 组合代替继承捆绑 | 2.3 两层继承固定顺序 | protected IChannel channel; 构造器注入 |
| ④ 通用逻辑只写一处 | 2.1/2.3 到处复制 | IOSPusher 中写一次退避,所有通道共享 |
| ⑤ 运行时动态选择 | 2.2 编译期硬编码 | new IOSPusher(new WhatsAppChannel()) 运行时组合 |
桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联来取代传统的多层继承,将类之间的静态继承关系转变为动态的组合关系,使得系统更加灵活,并易于扩展,有效地控制了系统中类的个数(避免了继承层次的指数级爆炸)。更多内容 (opens new window)
# 3.2 桥接模式的标准骨架
上面 5 条约束翻译成代码,所有实现变体共用一个骨架:
// ① 实现层接口——维 B 的演化线
public interface Implementor {
void operationImpl(); // ③ 每个实现独立定义
}
// ② 具体实现——维 B 的成员
public class ConcreteImplementorA implements Implementor {
public void operationImpl() { /* A 的具体实现 */ }
}
public class ConcreteImplementorB implements Implementor {
public void operationImpl() { /* B 的具体实现 */ }
}
// ③ 抽象层——维 A 的演化线,持有实现层引用(组合桥)
public abstract class Abstraction {
protected Implementor impl; // ⑤ 组合桥接,非继承
public Abstraction(Implementor impl) { this.impl = impl; }
public abstract void operation(); // ④ 通用逻辑只在此体系写一次
}
// ④ 扩充抽象——维 A 的成员
public class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(Implementor impl) { super(impl); }
public void operation() {
// ④ 维 A 的通用逻辑(如 iOS 退避)只在此处一次
impl.operationImpl(); // ③ 委托给实现层
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
三句话记住:两条线(抽象线 + 实现线各自独立继承)→ 一个桥(抽象层持有实现层引用,组合非继承)→ 乘法变加法(加新维 A 成员只影响抽象线,加新维 B 成员只影响实现线)。差异全在"抽象层要不要有接口"和"实现层是接口还是抽象类"里头——这就是下一节四种实现的核心分岔。
桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
# 3.3 典型使用场景
不是所有"有两个维度"的系统都适合桥接。核心判断标准是:两个维度各自独立、都会持续扩展。以下场景验证:
- 图形界面库:控件(按钮/文本框/下拉框)与主题(Light/Dark)——两个维度独立变化,桥接后控件不加主题子类。
- 数据库访问层:JDBC 的
Connection/Statement(抽象层)与Driver(实现层)——加新数据库只需加 1 个 Driver,不碰 Connection。 - 消息传输协议:消息类型 × 传输方式(TCP/HTTP/MQTT)——两个维度各自扩展。
- 音频视频播放器:播放器(MP3/FLAC/AAC)× 平台实现(Android/iOS/Windows)。
反面提醒:只有一个维度在变、维度高度耦合(端 A 只能用通道 X)、类数量本来就少——参考 06 / 07 节。
# 04.四种实现对比
# 4.1 实现核心要点
四种写法本质上是在 抽象层形式 / 实现层形式 / 组合绑定方式 上的不同取舍。实现桥接模式的核心只要两件事:
Abstraction bridge = new RefinedAbstraction(new ConcreteImplementor()); // ① 双维组合
bridge.operation(); // ② 抽象层驱动
2
差异全在"抽象层是抽象类还是接口"和"组合是在构造器注入还是 setter 注入"这两个决策点里。下面按演进顺序逐一展开,最后在 7.2 节 会有一张决策图帮你快速定位。
# 4.2 实现A:经典四角色(支付案例)
设计权衡:用"四个角色的完整骨架"换"双维度扩展时零摩擦"。
选它的理由:两维都有 3+ 个成员且还在增长——经典四角色是最标准、最不易翻车的写法。
以支付场景为例:支付渠道 × 支付方式。实现层接口(支付方式):更多内容 (opens new window)
public interface IPayMode {
boolean security(String uId); // 安全校验:各支付模式的风控
}
2
3
具体实现(密码/刷脸/指纹):
public class PayCypher implements IPayMode {
@Override public boolean security(String uId) { return false; }
}
public class PayFaceMode implements IPayMode {
@Override public boolean security(String uId) { return true; }
}
public class PayFingerprintMode implements IPayMode {
@Override public boolean security(String uId) { return false; }
}
2
3
4
5
6
7
8
9
抽象层(支付渠道):
public abstract class Pay {
protected IPayMode payMode; // ① 组合桥接实现层
public Pay(IPayMode payMode) {
this.payMode = payMode;
}
public abstract String transfer(String uId, String tradeId, BigDecimal amount);
}
2
3
4
5
6
7
8
9
扩充抽象(微信/支付宝):
public class WxPay extends Pay {
public WxPay(IPayMode payMode) { super(payMode); }
@Override
public String transfer(String uId, String tradeId, BigDecimal amount) {
System.out.println("微信渠道支付划账开始......");
boolean security = payMode.security(uId); // ② 委托实现层
if (!security) {
System.out.println("微信渠道支付划账失败!");
return "500";
}
System.out.println("微信渠道划账成功! 金额: " + amount);
return "200";
}
}
public class ZfbPay extends Pay {
public ZfbPay(IPayMode payMode) { super(payMode); }
@Override
public String transfer(String uId, String tradeId, BigDecimal amount) {
System.out.println("支付宝渠道支付划账开始......");
boolean security = payMode.security(uId);
if (!security) {
System.out.println("支付宝渠道支付划账失败!");
return "500";
}
System.out.println("支付宝渠道划账成功! 金额: " + amount);
return "200";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
客户端——运行时组合:
private void test() {
System.out.println("测试: 微信支付、人脸方式");
Pay wxpay = new WxPay(new PayFaceMode());
wxpay.transfer("weixin", "10001900", new BigDecimal(100));
System.out.println("测试: 支付宝支付、指纹方式");
Pay zfbPay = new ZfbPay(new PayFingerprintMode());
zfbPay.transfer("zhifubao", "567689999999", new BigDecimal(200));
}
2
3
4
5
6
7
8
9
技术分析:
- 四角色:Abstraction(Pay) / RefinedAbstraction(WxPay,ZfbPay) / Implementor(IPayMode) / ConcreteImplementor(PayFace等)
- 支付渠道和支付方式的各自扩展互不影响——加"刷掌支付"只需加一个
IPayMode实现,不改Pay/WxPay - 代价:四个角色文件较多,但换来的是双维独立演化的"零摩擦"
# 4.3 实现B:SPI驱动桥接(JDBC/SLF4J风格)
设计权衡:用"运行时 SPI 发现实现"换"零代码更改即可切换底层实现"。
选它的理由:实现层由第三方提供(数据库驱动 / 日志实现 / MQ厂商),抽象层不直接 new 具体实现,而是在运行时通过 SPI 发现。
这是 JDBC 的核心架构:更多内容 (opens new window)
// ① 抽象层——Java 标准库提供的 SPI
public interface Connection extends Wrapper, AutoCloseable {
Statement createStatement();
PreparedStatement prepareStatement(String sql);
// ...
}
public interface Driver {
Connection connect(String url, java.util.Properties info);
boolean acceptsURL(String url);
}
// ② 实现层——MySQL 厂商提供(mysql-connector-java.jar)
public class MySQLDriver implements Driver {
public Connection connect(String url, Properties info) {
return new MySQLConnection(url, info); // ③ 具体实现
}
}
// ③ 客户端运行时组合——通过 URL 选择实现
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db");
// DriverManager 用 SPI 发现了 MySQLDriver → 组合成了 Bridge
// 切换到 PostgreSQL:
Connection conn = DriverManager.getConnection("jdbc:postgresql://localhost/db");
// 只改 URL,代码零改动——SPI 发现了 PostgreSQLDriver
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
技术分析:
- 这是"最像教科书桥接"的真实案例:
DriverManager是桥接控制器,Connection是抽象层,Driver是实现层 - 加新厂商驱动 = 加一个 jar + 一行 URL 变化,不改
Connection和DriverManager源码 - 代价:SPI 机制本身有加载顺序和类隔离的复杂度
# 4.4 实现C:轻量组合桥接(Shape×Color)
设计权衡:用"没有抽象父类的独立实现层"换"代码量最少"。
选它的理由:双维各只有 2-3 个成员,不会大规模扩展——轻量组合够用不复杂。
// ① 实现层——颜色,独立演化
public interface IColor {
void fill();
}
public class Red implements IColor {
public void fill() { System.out.println("填充红色"); }
}
public class Blue implements IColor {
public void fill() { System.out.println("填充蓝色"); }
}
// ② 抽象层——形状,持有颜色引用
public abstract class Shape {
protected IColor color; // ③ 桥接
public Shape(IColor color) { this.color = color; }
public abstract void draw();
}
// ④ 具体形状
public class Circle extends Shape {
public Circle(IColor color) { super(color); }
public void draw() {
System.out.print("画圆形 → ");
color.fill(); // ③ 委托实现层
}
}
public class Square extends Shape {
public Square(IColor color) { super(color); }
public void draw() {
System.out.print("画方形 → ");
color.fill();
}
}
// ⑤ 客户端
Shape redCircle = new Circle(new Red());
Shape blueSquare = new Square(new Blue());
redCircle.draw(); // 画圆形 → 填充红色
blueSquare.draw(); // 画方形 → 填充蓝色
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
技术分析:
- 相比经典四角色,省略了 RefinedAbstraction 和 Implementor 的显式分层——Shape 直接就是抽象层,IColor 直接就是实现层
- 适合双维成员少、扩展不频繁的场景
- 代价:如果形状或颜色数量暴涨,不如经典四角色的结构清晰
# 4.5 实现D:接口+内部类桥接
设计权衡:用"内部类实现接口"换"极度精简的文件数"。
选它的理由:小工具/小功能,连创建独立实现类都觉得多余——一个文件搞定桥接。
// ① 抽象层——形状接口
interface Shape {
void draw();
}
// ② 抽象基础——持有颜色引用
abstract class AbstractShape {
protected Shape colorImpl; // ③ 桥接到"颜色接口"
public void setColorImpl(Shape impl) { this.colorImpl = impl; }
public abstract void draw();
}
// ④ 具体形状
class Rectangle extends AbstractShape {
public void draw() { colorImpl.draw(); } // ③ 委托颜色绘制
}
class Circle extends AbstractShape {
public void draw() { colorImpl.draw(); }
}
// ⑤ 内部类实现"颜色"
class RedColor implements Shape {
public void draw() { System.out.println("绘制红色形状"); }
}
class BlueColor implements Shape {
public void draw() { System.out.println("绘制蓝色形状"); }
}
// 客户端
AbstractShape rect = new Rectangle();
rect.setColorImpl(new RedColor());
rect.draw(); // 绘制红色形状
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
技术分析:
- 实现层复用同一个
Shape接口——颜色也是一个Shape(画一个有颜色的图形 = 形状.draw() → 颜色.draw()) - 适合极简场景,少于 5 个类的桥接
- 代价:
Shape接口语义过载——既是形状又是颜色,读起来需要注释解释
# 4.6 四种实现速查表
| 实现方式 | 抽象层 | 实现层 | 扩展力 | 适合场景 | 推荐度 |
|---|---|---|---|---|---|
| A. 经典四角色 | 抽象类+子类 | 接口+实现类 | 最强 | 双维 3+ 成员,长期扩展 | ⭐⭐⭐⭐⭐ |
| B. SPI驱动桥接 | JDBC DriverManager | 厂商 Driver jar | 最强 | 实现层由第三方提供 | ⭐⭐⭐⭐⭐ |
| C. 轻量组合 | 抽象类+子类 | 接口+实现类 | 中等 | 双维各 2-3 成员 | ⭐⭐⭐⭐ |
| D. 接口+内部类 | 接口+抽象基类 | 同一接口实现 | 偏弱 | 微型工具,< 5 个类 | ⭐⭐⭐ |
📌 一句话决策:生产级双维度系统 → A. 经典四角色或B. SPI桥接;快速原型 → C. 轻量组合;微型工具 → D。
# 05.用前用后效果对比
为什么单独留一节做对比:很多人记住了"桥接"两个字,却没算过它到底把"乘法"省成了"加法"。下面用 1.x 节的推送系统做基准,让数据替你回答。
# 5.1 核心数据对比
实验设定:消息推送中台,4 端 × 4 通道 → 扩张到 5 端 × 7 通道。
| 维度 | ❌ 继承绑维度(事故现场) | ✅ 桥接模式 | 差距 |
|---|---|---|---|
| 初始类数(4×4) | 16 个子类 | 4 端 + 4 通道 = 8 个类 | 2× 减少 |
| 加 3 个新通道 | 新增 4 × 3 = 12 个子类 | 新增 3 个 Channel | 4× 收敛 |
| 再加 1 个新端 | 新增 1 × 7 = 7 个子类 | 新增 1 个 Pusher | 7× 收敛 |
| 最终类数(5×7) | 16 + 12 + 7 = 35 个 | 5 + 7 = 12 个 | 3× 减少 |
| 改"退避算法" | 35 个类全改 | 改 1 处 IOSPusher | 35× 收敛 |
| 端×通道动态切换 | 不支持(编译期固定) | new IOSPusher(new LineChannel()) 任意组合 | — |
| 单元测试用例数 | 35 套 | 5 + 7 = 12 套 + 烟雾测试 | 3× 减少 |
| 代码复用 | 16 处 iOS 退避各写一份 | iOS 退避只在 IOSPusher 一次 | 根本性消除 |
# 5.2 核心收益
🔑 核心收益:桥接的本质是 "把乘法变成加法"——把"维度组合"从编译期固定的继承树,转化为运行期组合的两条独立轴。一边可以加无数个实现(通道/模式/驱动),另一边可以加无数个抽象(端/渠道/连接器),两边互不影响。
这就是为什么 JDBC 用
DriverManager × Driver、SLF4J 用Logger × Binding撑起整个生态:一边加数据库驱动,另一边加连接池/事务管理器,彼此独立。桥接不是"高级炫技",是"在双维度场景下唯一不爆炸的解法"。
# 06.反面踩坑实录
为什么有这一节:01 节让你看到"不用桥接的痛",但桥接本身也不是银弹。本节用 4 个真实事故告诉你"乱用的痛"。
# 6.1 踩坑A:把非正交维度硬拆成桥接
【真实事故】 抽象层被迫判断实现层类型:
public abstract class Pusher {
protected Channel channel;
public void send(Message msg) {
if (channel instanceof WeChatChannel) { // ❌ 抽象层判断实现层类型
((WeChatChannel) channel).appendOpenId(msg);
}
channel.send(msg);
}
}
2
3
4
5
6
7
8
9
💣 事故现场:Pusher 里 instanceof 了 5 种 Channel 类型 → 加新通道不只要实现 Interface,还得改 Pusher 的 if-else → 桥接完全白做了。
📌 教训:桥接的前提是 "两个维度真的独立"。如果"端"必须知道"通道"的具体细节才能工作,那它们根本不正交,强拆桥接反而比继承还乱。
✅ 正解:先用 PoC 写 5 个组合,看能否各自独立扩展——如果 Pusher 必须 instanceof 判断 Channel 类型,请回退到继承或策略模式。
# 6.2 踩坑B:抽象层退化成空壳
【真实事故】 抽象层只有一行委托调用:
public abstract class Pay {
protected IPayMode payMode;
public abstract String transfer(...);
}
public class WxPay extends Pay {
public String transfer(...) {
return payMode.security(uId); // ❌ 抽象层一行代码,啥都没做
}
}
2
3
4
5
6
7
8
9
💣 事故现场:线上支付,WxPay 里就是转发给 IPayMode,连日志都没打 → 抽象层完全没价值 → 整个桥接退化成"组合调用器"。
📌 教训:桥接的"抽象层"应该承载 "端独有的非平凡逻辑"(iOS 的退避算法、PC 的连接复用、风控聚合)。如果抽象层只是"调用一下实现层",继承都不用,直接组合 1 个字段即可。
✅ 正解:抽象层必须有"自己的非平凡逻辑",否则桥接就是过度设计——直接用组合模式即可。
# 6.3 踩坑C:客户端写死组合,桥接灵活性归零
【真实事故】 客户端直接 new 具体组合:
// ❌ 客户端硬编码组合,桥接的"运行时灵活性"全废
Pay pay = new WxPay(new PayFaceMode());
2
💣 事故现场:运营要上线 A/B 测试——灰度组用微信+人脸、对照组用微信+密码,但代码写死了组合 → 改代码 + 重新发版 → A/B 测试延迟 2 天。
📌 教训:桥接的核心价值之一是"运行时动态组合"——根据用户配置/AB 实验/国家/渠道灵活选择。如果客户端写死 new,退化成"两层继承+组合",灵活性归零。
✅ 正解:用工厂方法 + 配置中心动态拼装:
public class PayFactory {
public static Pay create(String channelType, String modeType) {
IPayMode mode = ModeFactory.create(modeType); // 配置中心决定
if ("WX".equals(channelType)) return new WxPay(mode);
if ("ALI".equals(channelType)) return new ZfbPay(mode);
throw new IllegalArgumentException(channelType);
}
}
2
3
4
5
6
7
8
# 6.4 踩坑D:桥接和适配器写在一起
【真实事故】 一个类既桥接又适配:
// ❌ 既"对接老接口"又"做维度拆分",代码看不懂
public class LegacyPayAdapter extends Pay {
private OldPaySDK sdk; // 适配老 SDK
private IPayMode mode; // 桥接维度
public String transfer(...) {
sdk.oldDoPay(...); // 适配器职责
return mode.security(...); // 桥接职责
}
}
2
3
4
5
6
7
8
9
💣 事故现场:新人接手后完全理不清——到底是适配老接口还是在做双维桥接?→ 不敢改,怕牵一发动全身。
📌 教训:桥接和适配器外形非常像,但桥接是设计阶段就规划好的双维拆分,适配器是后期补救兼容的临时桥。把两者混在一个类里,职责不清。
✅ 正解:分两层——先用 Adapter 把老 SDK 包成新 IPayMode,再让 Pay 桥接到 IPayMode,职责分离:
class OldPayModeAdapter implements IPayMode { // 第一步:适配
private OldPaySDK sdk;
public boolean security(String uId) {
return sdk.oldRiskCheck(uId);
}
}
Pay pay = new WxPay(new OldPayModeAdapter()); // 第二步:桥接组合
2
3
4
5
6
7
# 6.5 替代方案汇总
| 你的需求 | 推荐方案 |
|---|---|
| 只有 1 个维度在变(如只有形状没颜色) | ✅ 直接继承即可 |
| 双维但高度耦合(端A只能用通道X) | ✅ 继承反而清晰 |
| 维 A 是"算法可替换"而非独立实例 | ✅ 策略模式(14 篇) |
| 想给单个对象动态加责任 | ✅ 装饰者模式(08 篇) |
| 双维独立、都会持续扩展 | ✅ 桥接模式 |
# 07.决策树与选型
经过前面 6 节的铺垫,是时候给一张能"贴在工位上"的决策图了。
# 7.1 该不该用桥接模式
flowchart TD
Start([类的变化维度<br/>超过 1 个?]) --> Q1{两个维度<br/>真的独立?}
Q1 -->|否| Inherit[✅ 直接用继承<br/>维度高度耦合不应硬拆]
Q1 -->|是| Q2{两维都会<br/>持续扩展?}
Q2 -->|否| Inherit2[✅ 继承或简单组合<br/>不扩展不必桥接]
Q2 -->|是| Q3{只是想<br/>拆算法可替换?}
Q3 -->|是| Strategy[✅ 用策略模式<br/>见第 14 篇]
Q3 -->|否| Q4{只想给对象<br/>动态加责任?}
Q4 -->|是| Decorator[✅ 用装饰者模式<br/>见第 08 篇]
Q4 -->|否| Q5{实现层由<br/>第三方提供?}
Q5 -->|是| SPI[✅ SPI驱动桥接<br/>如 JDBC/SLF4J 风格]
Q5 -->|否| Classic[✅ 经典四角色桥接]
style Inherit fill:#dfd
style Inherit2 fill:#dfd
style Strategy fill:#fff4e6
style Decorator fill:#fff4e6
style SPI fill:#f0e6ff
style Classic fill:#dfd
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 7.2 选哪种实现方式
如果决策树走到了"用桥接模式",再用下面这张图选实现:
flowchart TD
Start([选择桥接实现方式]) --> Q1{双维各有<br/>3+ 个成员?}
Q1 -->|否| Opt1[轻量组合桥接<br/>Shape×Color 极简风格<br/>双维各2-3个成员]
Q1 -->|是| Q2{实现层<br/>由第三方提供?}
Q2 -->|是| Opt2[SPI驱动桥接<br/>DriverManager + SPI<br/>JDBC/SLF4J 风格]
Q2 -->|否| Q3{需要文件<br/>极度精简?}
Q3 -->|是| Opt3[接口+内部类<br/>一个接口搞定双维<br/>微型工具场景]
Q3 -->|否| Opt4[经典四角色<br/>Abstraction+Implementor<br/>生产级首选]
style Opt1 fill:#e6f3ff
style Opt2 fill:#fff4e6
style Opt3 fill:#e6ffe6
style Opt4 fill:#f0e6ff
2
3
4
5
6
7
8
9
10
11
12
13
# 7.3 选型清单速查
| 场景 | 该用吗 | 推荐方式 |
|---|---|---|
| JDBC / SLF4J(厂商驱动 + 统一API) | ✅ 该用 | SPI驱动桥接(实现 B) |
| 支付渠道 × 支付方式 | ✅ 该用 | 经典四角色(实现 A) |
| UI控件 × 主题 / 平台 | ✅ 该用 | 经典四角色(实现 A) |
| 形状绘制 × 颜色(小项目) | ⚠️ 有条件 | 轻量组合(实现 C) |
| 只想拆算法可替换 | ❌ 别用 | 策略模式 |
| 双维高度耦合不可分 | ❌ 别用 | 继承 |
| 只有 1 个维度在变 | ❌ 别用 | 继承或组合 |
# 08.总结与延伸
# 8.1 设计思想沉淀
回顾本篇 01 → 07 的旅程,桥接模式真正教会我们的是这套思考模型:
| 阶段 | 学到了什么 |
|---|---|
| 01 事故引入 | 痛点是模式诞生的土壤——4端×4通道=16类→5端×7通道=35类爆炸 |
| 02 三次失败 | 纯继承、if-else判断、两层继承都不够——模式是从"试错"中收敛的 |
| 03 模式基础 | 三大要点:两条独立演化线 + 一个组合桥 + 乘法变加法 |
| 04 四种实现 | 实现差异本质是"抽象层形式 / 实现层形式 / 绑定方式"的不同权衡 |
| 05 效果对比 | 数据说话:35 类 → 12 类;改退避算法 1 处 vs 35 处 |
| 06 反面踩坑 | 桥接不是免死金牌:非正交维、空壳抽象、写死组合、桥接适配混淆 |
| 07 决策树 | 工程师的成熟度,不在于会写几种桥接,而在于知道"什么时候不上桥" |
🔑 一句话核心:
桥接模式是用来**把"两个独立变化维度的笛卡尔积组合拆成平行演化"**的,不是"任何有两个字段的类都该上桥接"。桥接的价值在于"双维都会持续扩展"——只有一边在变、或两边都不变,继承/组合更合适。
# 8.2 模式联动边界
桥接从来不是孤立存在的,它和其他模式有千丝万缕的关系:
flowchart LR
Adapter[适配器<br/>07篇] -.后期兼容.-> Bridge[桥接]
Strategy[策略<br/>14篇] -.单维算法.-> Bridge
Decorator[装饰者<br/>08篇] -.叠加增强.-> Bridge
Bridge -.创建对象族.-> AbsFactory[抽象工厂<br/>02篇]
2
3
4
5
| 模式 | 关系 | 一句话区别 |
|---|---|---|
| 适配器(07 篇) | 桥接是"前期分维度设计",适配器是"后期接口补丁" | 桥接预防类爆炸;适配器解决兼容 |
| 策略模式(14 篇) | 策略只关注"算法可替换"单维度 | 想换算法 → 策略;想拆双维度 → 桥接 |
| 装饰者(08 篇) | 装饰同接口"叠加增强",桥接是双层独立扩展 | 想加料 → 装饰;想分维 → 桥接 |
| 抽象工厂(02 篇) | 抽象工厂常创建桥接两侧的对象族 | 组合使用:工厂创建桥,桥连接双维 |
⚠️ 什么时候不该用桥接
- 维度只有 1 个:只有不同形状没有不同颜色 → 直接继承;
- 维度高度耦合:端 A 必须通道 X,端 B 必须通道 Y → 继承反而清晰;
- 类数量本来就少:3 × 3 = 9 个类一辈子不变,套桥接是过度设计;
- 抽象层无独立逻辑:如果"端"自己没事干,只是转发给"通道",那就直接组合,别套桥接。
一句话:桥接的价值在于"双维都会持续扩展"。如果只有一边扩展、或两边都不扩展,继承/组合更合适。桥接不是炫技,是"避免笛卡尔爆炸"的工程化兜底。
# 8.3 思考题与延伸
💭 三道思考题(建议手写答案,再对照回顾本文):
- JDBC 的
DriverManager.getConnection()一行代码做了"抽象层 × 实现层"的桥接——画出 JDBC 的桥接类图,标注 Abstraction / Implementor 分别是谁,以及 SPI 机制是如何发现Driver的?(提示:回看 4.3 节) - Spring 的
Resource接口有ClassPathResource / FileSystemResource / UrlResource等子类——这是桥接模式吗?如果是,桥的两个维度分别是什么?如果不是,为什么?(提示:回看 7.1 决策树) - 如果推送系统引入"消息优先级"作为第三个维度(紧急 / 普通 / 延迟),桥接模式还能处理吗?怎么处理?(提示:回看 4.1 节的核心要点——桥接只能处理两个维度)
📚 延伸阅读:
- 阅读 JDBC
DriverManager.getConnection()源码(30 行核心代码,教科书级桥接 + SPI) - 阅读 SLF4J
LoggerFactory.getLogger()的 SPI binding 机制(桥接 + 外观的组合应用) - 阅读 Spring
Resource体系(工程级桥接/策略混合体)
🔍 真实开源代码中的桥接模式:
| 出处 | 抽象层(Abstraction) | 实现层(Implementor) | 它桥接了什么 |
|---|---|---|---|
| JDBC | Connection / Statement / PreparedStatement 等 SPI | Driver 接口的 MySQL/Oracle/PostgreSQL/SQL Server 实现 | "数据库操作语义" × "厂商驱动实现" |
| SLF4J | Logger 接口 | Log4jLoggerAdapter / LogbackLogger / JulLogger | "日志门面 API" × "底层日志实现" |
Spring Resource | Resource 接口 | ClassPathResource / FileSystemResource / UrlResource | "资源访问统一抽象" × "资源物理位置" |
| AWT / Swing | Component / Window | ComponentPeer / WindowPeer 由各 OS 平台实现 | "GUI 控件抽象" × "OS 原生窗口实现" |
| Netty | Channel(Nio/Epoll/Oio) | EventLoop / ChannelPipeline | "Channel 行为抽象" × "IO 模型实现" |
| MyBatis | Executor(Simple/Reuse/Batch) | Transaction(JDBC/Managed/Spring) | "SQL 执行策略" × "事务管理实现" |
| Spring Cache | CacheManager × Cache | EhCacheManager / RedisCacheManager | "缓存管理抽象" × "厂商缓存实现" |
学习路径建议:先读 JDBC 的
DriverManager.getConnection()(30 行核心代码,教科书级桥接)→ 再读 SLF4J 的LoggerFactory.getLogger()(含 SPI binding,桥接 + Facade 组合)→ 最后读 SpringResource体系(抽象层 5 个方法,实现层 8+ 种资源位置,工程级巅峰)。读完这三个,你就理解了"桥接不是炫技,是避免笛卡尔爆炸的工程化兜底"。