JDBC 用了 30 年的 Bridge 模式,多数人以为它只是策略模式换了层皮

每次讲 Bridge 模式,底下总有人小声说"这不就是策略模式吗"。换个接口实现而已,有什么难的。

但你去问"JDBC 的 DriverManager 和 Driver 之间是什么关系",同一个能说出一堆设计模式的人,大部分答不上来。JDBC 就是 Bridge 模式的标准教科书案例,用了 30 年,每天都在你代码里跑,你却没认出它来。

Bridge 跟 Strategy 的区别不在于代码写法,在于它解决的问题维度。Strategy 是"一个维度上换实现",Bridge 是"两个维度同时变"。这个区别搞不清,你写的"Bridge"要么退化成 Strategy,要么变成继承爆炸的怪物。

先看一个让你继承爆炸的真实场景

假设你在做一个消息通知系统,需要支持两种维度:

  • 维度一:消息类型(短信、邮件、站内信、Push)
  • 维度二:发送渠道(阿里云、腾讯云、自建网关)

最直觉的写法是继承:

markdown 复制代码
Message
├── SMSMessage
│   ├── AliyunSMSMessage
│   ├── TencentSMSMessage
│   └── SelfHostedSMSMessage
├── EmailMessage
│   ├── AliyunEmailMessage
│   ├── TencentEmailMessage
│   └── SelfHostedEmailMessage
├── PushMessage
│   ├── AliyunPushMessage
│   ├── TencentPushMessage
│   └── SelfHostedPushMessage
└── InAppMessage
    ├── AliyunInAppMessage
    ├── TencentInAppMessage
    └── SelfHostedInAppMessage

4 种消息类型 × 3 个发送渠道 = 12 个类。加一种消息类型变成 15 个,加一个渠道变成 16 个。子类数量 = 类型数 × 渠道数,指数级膨胀。

这就是 Bridge 要解决的经典问题:两个独立变化的维度,用继承会把它们绑死,用组合才能解耦。

Bridge 的解法:把一个维度抽成接口

把"发送渠道"这个维度抽成接口,消息类型持有它的引用:

java 复制代码
// 实现维度:发送渠道
public interface MessageSender {
    void send(String to, String content);
}

public class AliyunSender implements MessageSender {
    public void send(String to, String content) {
        // 调阿里云 SMS/邮件/Push API
    }
}

public class TencentSender implements MessageSender {
    public void send(String to, String content) {
        // 调腾讯云 API
    }
}

public class SelfHostedSender implements MessageSender {
    public void send(String to, String content) {
        // 走自建网关
    }
}

// 抽象维度:消息类型
public abstract class Message {
    protected MessageSender sender; // 桥接点:持有实现维度

    public Message(MessageSender sender) {
        this.sender = sender;
    }

    public abstract void notify(String to, String content);
}

public class SMSMessage extends Message {
    public SMSMessage(MessageSender sender) {
        super(sender);
    }

    public void notify(String to, String content) {
        // 短信特有的处理:字数截断、签名追加
        String smsContent = truncateContent(content);
        sender.send(to, smsContent);
    }
}

public class EmailMessage extends Message {
    public EmailMessage(MessageSender sender) {
        super(sender);
    }

    public void notify(String to, String content) {
        // 邮件特有的处理:HTML 模板、附件
        String htmlContent = wrapHtml(content);
        sender.send(to, htmlContent);
    }
}

现在类数量变成 4 + 3 = 7 个。加一种消息类型加 1 个类,加一个渠道加 1 个类。两个维度独立扩展,互不影响。

Message 持有 MessageSender 的引用------这个引用就是"桥"。抽象维度(消息类型)通过这座桥调用实现维度(发送渠道),两者解耦但能通信。

JDBC:用了 30 年的 Bridge

JDBC 的设计就是标准的 Bridge 模式,只是它用了不同的名字:

复制代码
抽象维度(Abstraction):java.sql.DriverManager / java.sql.Connection
实现维度(Implementor):java.sql.Driver(各数据库厂商实现)

你写 Java 代码连数据库,从来没有直接 new MysqlDriver() 或者 new OracleDriver()。你是通过 DriverManager.getConnection() 拿到 Connection,然后操作数据库。

java 复制代码
// 你写的代码
Connection conn = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/mydb", "root", "password"
);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");

DriverManager 是抽象维度,Driver 是实现维度。MySQL 驱动、Oracle 驱动、PostgreSQL 驱动各自实现 java.sql.Driver 接口,通过 SPI 机制注册到 DriverManager 里。

java 复制代码
// MySQL 驱动里的注册代码(com.mysql.cj.jdbc.Driver)
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

你换数据库的时候不用改业务代码,只换驱动 JAR 包和连接字符串。两个维度独立变化:JDBC API(抽象维度)升级不影响你用的驱动(实现维度),驱动版本更新不影响你写的 JDBC 调用代码。

这就是 Bridge 的工程价值------不是炫技,是让你的代码能跨数据库运行。

Bridge 和 Strategy 到底差在哪

代码结构上,Bridge 和 Strategy 长得几乎一样:都是一个类持有另一个接口的引用,通过组合代替继承。

区别在语义和场景:

Strategy 是"一个维度换实现"

  • 你有一个排序功能,可以在运行时切换快速排序、归并排序、堆排序
  • 排序策略是可替换的,但"排序"这个行为本身不变
  • 策略对象通常是短生命周期的,用完就丢

Bridge 是"两个维度同时变"

  • 你有消息类型和发送渠道两个独立变化的维度
  • 两个维度都需要独立扩展,不是简单的"换一个实现"
  • 桥接对象通常是长生命周期的,跟宿主对象同生共死

判断标准很简单:如果你只有一个变化维度,用 Strategy。如果你有两个独立变化维度,用 Bridge。如果你把 Bridge 用在只有一个维度的场景上,它就退化成了 Strategy------不是错,是没必要。

三个真实踩坑

坑一:把实现维度做成静态工具类

有人觉得 MessageSender 只是转发调用,做成接口太重了,直接用静态工具类:

java 复制代码
public class SendUtils {
    public static void sendByAliyun(String to, String content) { ... }
    public static void sendByTencent(String to, String content) { ... }
}

然后在 SMSMessageif (channel.equals("aliyun")) SendUtils.sendByAliyun(...)

这就回到了 if-else 地狱,Bridge 的意义全部丢失。实现维度必须是接口 + 多实现,这样才能通过依赖注入替换,才能单元测试时 mock。

坑二:抽象维度知道太多实现细节

Bridge 的核心是抽象维度不该知道实现维度的细节。但有人写着写着就在抽象类里写了实现特定的逻辑:

java 复制代码
public abstract class Message {
    protected MessageSender sender;

    public void notify(String to, String content) {
        if (sender instanceof AliyunSender) {
            // 阿里云限流:每秒最多 100 条
            rateLimiter.acquire();
        }
        sender.send(to, content);
    }
}

instanceof 一出现,Bridge 的抽象-实现分离就破了。限流逻辑应该放在 AliyunSender 内部,不该泄漏到抽象层。抽象维度只该定义"做什么",不关心"怎么做"。

坑三:桥接对象的生命周期管理

Bridge 模式里,抽象对象持有实现对象的引用。如果实现对象持有资源(连接池、文件句柄),生命周期管理就变重要了。

JDBC 的 Connection 就是个典型。Connection 是抽象维度,它内部持有驱动厂商提供的实现对象。Connection.close() 关的不只是你的连接,还有底层的网络连接和资源。

一个常见 bug:在连接池场景下,Connection.close() 不是真关连接,而是还回连接池。如果你自己实现 Bridge 模式时没考虑这种"归还而非销毁"的语义,资源泄漏是迟早的事。

什么时候该用 Bridge

不是所有两个维度的场景都需要 Bridge。判断标准:

  1. 两个维度都在独立变化,且未来大概率会扩展 → 用 Bridge
  2. 两个维度中一个稳定一个变化 → 只对变化维度用 Strategy 就够了
  3. 两个维度都稳定 → 直接继承,别过度设计

JDBC 适合 Bridge 是因为:API 维度(Connection/Statement/ResultSet)和驱动维度(MySQL/Oracle/PostgreSQL)都在独立演进。JDBC 4.0 到 4.3 的 API 升级和 MySQL 驱动 5.x 到 8.x 的版本升级是两条独立的线,Bridge 让它们互不干扰。

消息通知系统适合 Bridge 是因为:消息类型和发送渠道都是业务驱动的高频变化点。今天加一种 Push 消息,明天换一个华为云渠道,两条线各自迭代。

如果两个维度中有一个几乎不变,Bridge 就是杀鸡用牛刀。别为了设计模式而设计模式。

Bridge 的本质就一句话:用组合代替继承,让两个维度各自扩展。 JDBC 靠这个设计跑了 30 年没被重构,不是因为 Bridge 多精妙,是因为它解决了真实的多维度扩展问题。

顺便说一句,我在做的那个小程序「爪爪代码冒险记」里,Bridge 模式是用卡皮巴拉过河找桥来讲的------河两边各自独立,中间架一座桥才能通行。比直接讲 JDBC 好理解一点,感兴趣可以搜搜看。

相关推荐
程序员鱼皮11 小时前
3 大 DeepSeek Harness 进阶玩法,招多个大肥鱼帮我干活!
前端·后端·ai编程
苍何11 小时前
DeepSeek 终于支持多模态了(附实测及接入教程)
后端
KoPa11 小时前
HeySmart:大模型开源网关基座-请求生命周期与钩子引擎
前端·后端
苍何11 小时前
做AI视频还在拆盲盒?手把手教你导演级运镜(附教程)
后端
爱学习的小邓同学11 小时前
Golang语言入门
开发语言·后端·golang
苍何11 小时前
豆包,开始做普通人的 Codex
后端
七牛开发者11 小时前
Coding Agent 如何跑稳长任务?从上下文管理到运行时状态
前端·javascript·后端
Zane199411 小时前
遍历时删元素为什么报错:modCount 这个"计数器"在背后盯着你
java·后端
苍何11 小时前
原来世界模型,已经能边玩边生成了
后端
七牛开发者11 小时前
拆解 DeepSeek Harness:Profile 与 Bundle 如何装配运行时
前端·javascript·后端