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 好理解一点,感兴趣可以搜搜看。

相关推荐
Ai拆代码的曹操1 小时前
排查 3 小时,问题竟在 Dubbo 路由规则:一个 force 的坑
后端
无责任此方_修行中1 小时前
换个版本号就能升级?可没那简单:pnpm 11 升级踩坑记
javascript·后端·npm
SelectDB1 小时前
洋钱罐基于 SelectDB 实现 Hive 数据湖透明加速:查询 P95 从 300 秒降至 20 秒的完整实践
后端
Chen_LSN2 小时前
C语言——深度理解指针(4)
后端
用户638982245892 小时前
自定义注解@SyncNeo4j实现增删改导入时自动同步数据节点和关系到Neo4j
后端
AlunYegeer2 小时前
Spring Boot 多模块服务间 API Token 鉴权排查记录
java·spring boot·后端
沙湖遇雨2 小时前
3.客户端向服务端建立连接
后端
沙湖遇雨2 小时前
1.netty源码阅读-管理端Server启动
后端
玖石书2 小时前
ASP.NET Core迁移Spring系列:运行时与 GC 篇
后端·spring·asp.net