99% 的人把桥接模式当策略模式用------抽象与实现分离,你做的不是同一件事
桥接模式(Bridge)在 GoF 23 种里属于"学过就忘"的典型。网上搜"桥接模式",前 10 条结果有 8 条在画同一个 UML:Abstraction → RefinedAbstraction / Implementor → ConcreteImplementor,然后一段代码演示"如何切换实现"。看完整个人是懵的------这跟策略模式到底有什么区别?
我在生产代码里见过桥接模式的次数,大概不超过 5 次。其中 3 次是把策略模式当桥接用。剩下 2 次是用错了场景,硬上桥接结果把代码搞复杂。
桥接模式不是没用,是它的适用场景太窄,能正确识别场景的人更少。
桥接模式到底在解决什么
桥接模式的官方定义是"将抽象部分与实现部分分离,使它们都可以独立变化"。听起来很玄,本质上是:两个独立变化的维度,用组合而不是继承来组织。
经典的例子是"图形 × 颜色"。图形有圆形、方形、三角形,颜色有红色、蓝色、绿色。如果用继承,你会得到 Circle-Red、Circle-Blue、Square-Red、Square-Blue......颜色和图形每加一个都要派生一堆新类,类数量呈笛卡尔积爆炸。
桥接模式的做法是把"图形"和"颜色"拆成两棵树,用组合把它们连起来:
java
abstract class Shape {
protected Color color; // 桥:组合而非继承
public Shape(Color color) { this.color = color; }
abstract void draw();
}
class Circle extends Shape {
public Circle(Color color) { super(color); }
public void draw() {
System.out.print("Circle - ");
color.apply();
}
}
interface Color {
void apply();
}
class RedColor implements Color {
public void apply() { System.out.println("Red"); }
}
新加一种颜色?只需要新增一个 RedColor 实现类,所有 Shape 自动支持。新加一种形状?只需要新增一个 Shape 子类,所有 Color 自动支持。笛卡尔积消失。
这就是桥接模式的核心:两个维度必须同时支持独立扩展。图形家族扩展不影响颜色家族,颜色家族扩展不影响图形家族,两者通过一个"桥"(组合关系)连接。
跟策略模式的本质区别
这是最常被搞混的地方。策略模式也是"组合替代继承",也是"切换实现"。乍一看跟桥接没区别。
区别在于:策略模式的"实现"是一次性的、被替换的;桥接模式的"实现"是有状态的、被多个抽象共享的。
策略模式举例:你有多种排序算法(冒泡、快排、归并),让客户端选择用哪种。算法之间互相独立,一次只用一个。上下文 Sorter 持有一个 SortStrategy,调用 strategy.sort(data) 就完事了。
桥接模式举例:JDBC 驱动。一个 JDBC 连接对应一个具体数据库驱动(MySQL、Oracle、PostgreSQL),但驱动不是一次性的------它有自己的连接池、事务管理、元数据缓存。这个驱动对象被多个 JDBC API 调用共享,有自己的生命周期和状态。
策略模式的实现是 stateless 的(或者状态不重要),用完即弃。桥接模式的实现是有状态的、有生命周期的、被复用的。
如果你看到一段代码用 Strategy strategy = new ConcreteStrategy() 然后立刻 context.doWork(strategy)------这是策略。如果看到 Implementor impl = factory.create(); Abstraction a = new RefinedAbstraction(impl); ... 而且 impl 被多个地方共享、跨多个调用存活------这是桥接。
很多教程没把这两点讲清楚,所以你看到的代码明明是策略模式,硬套上"桥接模式"的帽子,结果评论区一片"这不就是策略吗"。
为什么生产代码里几乎见不到桥接模式
理解了定义和跟策略的区别之后,下一个问题来了:为什么我读过的 Java 生产代码里,几乎没有人显式使用桥接模式?
三个原因。
原因一:大多数项目只有一个维度在变。
桥接模式的前提是两个独立的扩展维度。但实际项目里,80% 的需求只有一个维度在变------要么业务类型在变(这是策略/工厂),要么抽象层级在变(这是继承)。两个维度同时变化且互不影响的场景,本来就少。
JDBC、SLF4J、Spring 的 BeanFactory 这些"教科书桥接例子"是基础设施级别的抽象,由框架作者提前做好。普通业务开发者一辈子不需要自己写桥接模式。
原因二:JDK 和框架已经把桥接模式实现完了。
Java 自身的 API 大量使用桥接模式,只是你看不到:
- JDBC:
DriverManager(抽象)/Driver(实现)。连接数据库时你写DriverManager.getConnection(url),背后是 JDBC 桥接在跑。 - AWT:
Component(抽象)/Peer(实现,平台相关的本地组件)。这是经典的桥接。 - SLF4J:
Logger(抽象)/LoggerFactory绑定具体日志实现(Logback、Log4j)。
你日常写代码天天在"用"桥接模式,只是你用的是 JDK 写好的版本,自己不需要再写一遍。所以桥接模式在教科书里显得很重要,在生产代码里显得"几乎见不到"------因为生产代码里的桥接已经被框架吃掉了。
原因三:不假思索地用桥接反而过度设计。
如果你看到一段代码只有 3-5 个形状 × 3-5 个颜色,硬上桥接模式,结果是多了 10 个类,逻辑反而更绕。桥接模式的"两个维度独立扩展"前提是预计每个维度都会持续加新成员。如果你的形状只有 3 种固定不变,5 个具体类硬编码就完了,桥接模式纯属添乱。
什么时候该用桥接模式
基于以上分析,桥接模式的真实适用场景需要同时满足:
两个维度必须独立扩展。这是硬条件。如果你的业务里只有一个维度在变,桥接模式过度设计。
每个维度预计会持续增加新成员。不是 3-5 个固定项,是会随业务成长。JDBC 桥接是因为数据库会持续新增,桥接模式才能长期发挥作用。
实现部分需要有自己的状态和生命周期。这是区分策略的关键------桥接的实现不能是 stateless 的一次性对象。
抽象部分要能跨多个实现工作。桥接的好处是抽象部分不关心具体实现,"一个抽象,多个实现共存"是常态。
满足这四个条件,桥接模式就是合适的。比如:
- 跨平台 UI 组件:组件类型(按钮、输入框)× 平台(Windows、macOS、Linux),每个维度都持续扩展,实现部分有平台相关的资源句柄。
- 跨数据库 ORM:业务实体类型 × 数据库方言(MySQL、Oracle、PostgreSQL),方言有自己的元数据缓存和方言 SQL 生成器。
- 多渠道消息推送:消息类型(订单、物流、营销)× 渠道(短信、邮件、Push、微信公众号),每边都有持续扩展需求。
这些场景下桥接模式能带来明显的可维护性收益------新加一个维度不会触发其他维度的连锁修改。
桥接模式最常见的错误用法
错误一:跟策略模式混用。
我见过最离谱的一段代码:AbstractService 持有一个 Strategy strategy,然后 Service 类居然有 setStrategy() 方法动态切换------这百分之百是策略模式,不是桥接。桥接的实现是在创建时绑定的,不应该支持运行期切换(如果需要运行期切换,那是策略 + 抽象工厂的组合)。
错误二:滥用"两个维度"前提。
很多人写代码的时候为了用桥接而硬凑两个维度。比如"用户类型 × 用户状态"------这两个维度根本不独立,新加一种用户类型不影响现有用户状态,用继承就够了,硬上桥接是过度设计。
错误三:在抽象层硬编码实现细节。
桥接模式要求抽象层完全不知道实现层的存在,但很多实现里抽象层会有类似 if (color instanceof RedColor) 的判断,这就破坏了桥接的核心约束。正确做法是把"颜色相关"的逻辑全部下沉到 Color 实现,Shape 只调用 color.apply(),不关心实现。
一个生产级的桥接实现参考
如果你确实遇到桥接模式的适用场景,下面这个模板可以参考:
java
// 抽象层:业务接口,不依赖任何实现细节
public abstract class NotificationChannel {
protected MessageSender sender; // 桥:组合而非继承
protected NotificationChannel(MessageSender sender) {
this.sender = sender;
}
public abstract void notify(User user, Message msg);
protected void doSend(User user, Message msg) {
// 抽象层通过 sender 发送,不关心具体渠道
sender.send(user, msg);
}
}
// 实现层:渠道实现,有自己的状态和生命周期
public interface MessageSender {
void send(User user, Message msg);
ChannelStats stats(); // 实现有自己的状态
}
public class WechatSender implements MessageSender {
private final AccessTokenCache tokens; // 实现自己的状态
public void send(User user, Message msg) {
var token = tokens.getFor(user.getAppId());
// 调用微信 API 发送
}
}
// 抽象层的具体类型
public class OrderNotification extends NotificationChannel {
public OrderNotification(MessageSender sender) {
super(sender);
}
public void notify(User user, Message msg) {
// 订单通知特有的逻辑(消息模板、优先级等)
msg.setTemplate("ORDER_{{id}}_TPL");
doSend(user, msg);
}
}
新加一个渠道?实现 MessageSender 接口就行。新加一种通知类型?继承 NotificationChannel 就行。两边独立扩展,互不影响。
桥接模式的"几乎用不到"不是因为它不重要,而是因为大多数开发者没有意识到 JDK 和框架里到处都是它。等你哪天需要写跨平台、跨数据库、跨渠道的基础组件时,你会发现桥接模式早就在那里等你了------只是你之前没认出来。
我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章。