设计模式 13 · 桥接模式

结构型模式的最后一个,是桥接模式(Bridge) 。它可能是七个结构型里最抽象、也最容易被讲得云里雾里的一个,但只要抓住它要解决的那个具体问题,就会豁然开朗。这个问题是:当一个东西同时在两个(或多个)独立的维度上变化时,如果用继承去表达,类的数量会"维度相乘"式爆炸。

我们在装饰器那篇见过一次类爆炸(2ⁿ 组合),但那是"同一维度的多个可选增强"的叠加。桥接面对的是另一种类爆炸,更常见也更隐蔽------两个正交维度的交叉 。举个我们订单场景里的例子:订单有不同类型 (普通订单、团购订单、预售订单),发通知又有不同渠道 (短信、App 推送、邮件)。这是两个完全独立的维度:订单类型的增减,和通知渠道的增减,毫不相干。可如果你用继承硬来,就会写出 普通订单短信通知普通订单App通知团购订单邮件通知...... 3 种类型 × 3 种渠道 = 9 个类,再加一种类型或渠道,就是 12 个、16 个,呈乘法增长。

桥接模式的核心思想,就是把这两个纠缠在一起的维度拆开 ,让它们各自独立地变化,再用一座"桥"把它们连起来。GoF 给它的正式定义是一句很经典但初看费解的话:"将抽象部分与它的实现部分分离,使它们都可以独立地变化。" 这一篇我们就把这句话彻底翻译成人话。

这篇文章按这条线索展开:先复现"两个维度用继承"导致的乘法类爆炸;再引出桥接如何把维度拆开、用组合搭一座桥;然后讲清那句"抽象与实现分离"到底在说什么(这是全篇最关键、也最容易误解的地方);接着看它的现实身影;最后辨析它与相近模式,给出适用边界,并为结构型模式画上句号。贯穿例是订单类型 × 通知渠道。

目录

  1. 两个维度用继承:乘法式类爆炸
  2. 桥接模式:把维度拆开,搭一座桥
  3. "抽象与实现分离"到底在说什么
  4. [现实身影:JDBC 与日志框架](#现实身影:JDBC 与日志框架)
  5. [桥接 vs 相近模式,以及什么时候用](#桥接 vs 相近模式,以及什么时候用)
  6. 结构型模式,到此收官

一、两个维度用继承:乘法式类爆炸

先把问题摆清楚。我们要做"给订单发通知"这件事,它牵涉两个维度:

  • 维度 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 个类(配齐三种订单)。每增加一个维度上的一项,类的数量就按另一个维度的规模成倍增加。 而且这些类里全是重复------NormalOrderSmsNotifyGroupOrderSmsNotify 里,"发短信"的代码几乎一样,只是订单文案不同。

问题的根源是:继承只有一条轴,却被迫同时表达两个维度。 你把"订单类型"和"通知渠道"这两件本来毫不相干的事,硬塞进了同一条继承链,于是它们被死死绑在一起,任何一个变,都牵动另一个。

正确的直觉应该是:这俩维度本来就该分开。 "订单类型"该有自己的一套类,"通知渠道"该有自己的一套类,然后在运行时把"某个类型"和"某个渠道"自由组合 起来------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 包里的 ConnectionStatementDriverManager 这套 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 的利器:策略模式

相关推荐
吠品1 小时前
Java中实现拓扑排序的两种方式:Kahn算法和DFS
java·数据库·notepad++
TunerT_TQ1 小时前
企业智能体工程体系v1.1|企业智能体工程卷 · 第5期· 数据飞轮——用观察—分析—计划—执行持续改进
java·运维·网络·#agent工程化·#企业智能体·#多智能体系统·#数据飞轮
小当家.1051 小时前
工具并行调用原理与实现:CompletableFuture 实战
java·agent·线程池·工具·并行
workflower2 小时前
全球云计算产业发展态势-市场:云计算规模保持稳步增长,产业格局日趋明晰
人工智能·深度学习·机器学习·设计模式·机器人·云计算
wangjialelele2 小时前
Spring Cloud 全体系学习笔记(REST、Nacos、Feign、Gateway)
java·spring boot·笔记·学习·spring·spring cloud·gateway
yk 坤帝2 小时前
用Python算清你的“买时间“账
java·服务器·python
weixin_BYSJ19872 小时前
springboot医疗信息管理系统---附源码17465
java·javascript·spring boot·python·django·flask·php
xqqxqxxq2 小时前
吃透 Spring 高频注解(包含SpringMVC Spring Boot)
java·spring boot·spring
都叫我大帅哥2 小时前
Embabel 1.0 Agent Framework 完全指南:从入门到生产级实践
java