99% 的人把桥接模式当策略模式用——抽象与实现分离,你做的不是同一件事

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 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章。

相关推荐
长栎1 小时前
你激活了 sharp-skills 一个模块,但你的项目从来不是单点活儿
后端
Lcos1 小时前
Kubernetes Toleration 六种写法详解:从精确匹配到全部放行
后端
元界metalite1 小时前
MyBatis事务只能靠Transactional吗-MetaLite为何只保留编程式事务
后端
用户9479135811621 小时前
20260821_090153_LangGraph_生产落地的_5_个关键坑:从_State
后端
Csvn1 小时前
📊 SQL 入门 Day 21:表设计与约束
后端·sql
SamDeepThinking1 小时前
警惕那些很长时间没有编写任何代码、却在设计系统的人
java·后端·架构
aloha_1 小时前
基于Spring Boot + Vue 3的前后端一体化部署方案
后端
星火10241 小时前
【Groovy翻译-进阶篇】Groovy 中的设计模式
后端·设计模式·groovy
Zane19941 小时前
ArrayList 插入慢,LinkedList 一定快吗
java·后端