桥接模式:让抽象与实现各自飞

桥接模式,说白了就是把「抽象」和「实现」拆开,让它们能各自独立变化。你想想看,我们做接口适配时,最头疼的是什么?就是需求一变,整个继承体系都得跟着改。桥接模式就是来解决这个问题的。

我记得刚工作那会儿,接手过一个老项目。对方系统升级了通信协议,我们这边得跟着适配。原来的代码把「消息类型」和「传输方式」绑死在继承树里,改一个地方,十几个子类都得动。那叫一个痛苦。后来重构时用了桥接模式,世界清净了。

桥接模式的核心:两个独立维度

桥接模式的核心思想,就是把一个事物中「会变化的两条线」拆开。一条线是抽象部分,另一条是实现部分。它们通过组合关系连接,而不是继承。

举个例子:

  • 抽象部分:消息的类型(普通消息、加密消息、签名消息)
  • 实现部分:消息的发送方式(HTTP、MQ、WebSocket)

这两个维度都会独立变化。今天加一种消息类型,明天换一种传输方式。如果用继承,就是 3×3=9 个子类。后天再加一种,就是 4×4=16 个。你受得了吗?

桥接模式的做法是:

  • 把「消息类型」做成抽象类,里面持有「发送方式」的引用
  • 把「发送方式」做成接口,各种传输方式去实现它
  • 消息类型要发消息时,直接调持有的发送方式接口

这样,新增消息类型不影响发送方式,新增发送方式也不影响消息类型。两个维度各自独立扩展。

核心公式:桥接模式 = 抽象维度 + 实现维度 + 组合桥接

变化维度数 = M + N,而不是 M × N

抽象化与实现化:到底谁是谁?

很多初学者会搞混「抽象」和「实现」在这里的含义。我简单解释一下:

  • 抽象化(Abstraction):定义「做什么」的接口或高层控制逻辑。比如「发送消息」这个行为,它定义了发送的流程:构建消息体、调用传输、处理结果。
  • 实现化(Implementor):定义「怎么做」的底层接口。比如「通过 HTTP 发送」、「通过 MQ 发送」,它们只负责把数据发出去,不关心消息长什么样。

抽象化持有实现化的引用,调用它的方法来完成具体工作。这就是「桥」——抽象化这边调用,实现化那边执行。

角色 职责 变化原因
Abstraction(抽象) 定义高层接口,持有 Implementor 业务需求变化(消息类型)
RefinedAbstraction(精确抽象) 扩展抽象类的功能 具体业务场景
Implementor(实现) 定义底层接口 技术选型变化
ConcreteImplementor(具体实现) 实现底层接口 具体传输方式

独立变化的维度:怎么识别?

我在项目中判断是否该用桥接模式,就看一个问题:这个类是不是有两个以上会独立变化的方向?

常见的独立维度组合:

  • 消息类型 × 传输方式
  • 数据格式 × 存储介质
  • 认证方式 × 密钥存储
  • 加密算法 × 密钥来源

嗯,这里要注意:不是所有两个维度都适合拆。如果两个维度变化频率差不多,或者变化方向很少,用继承反而更直观。桥接模式有它的适用场景。

我的经验:当你在代码里看到「if-else」或「switch」判断两个以上条件时,往往就是桥接模式该出场的时候。比如:

if (type == "加密") {
    if (transport == "HTTP") { ... }
    else if (transport == "MQ") { ... }
}

这种代码,就是桥接模式要消灭的坏味道。

桥接模式在接口适配中的应用

接口适配的场景,天然适合桥接模式。为什么?因为接口适配通常涉及两个变化维度:

  • 业务协议:不同接口的业务逻辑不同(查询、下单、通知)
  • 通信协议:不同系统的通信方式不同(HTTP REST、gRPC、MQ)

我做过一个支付网关的适配层,就是典型的桥接模式。抽象层定义「支付」接口(支付、退款、查询),实现层对接不同支付渠道(、支付宝、银联)。

来看一段简化代码:

// 实现化接口:支付渠道
interface PaymentChannel {
    boolean send(String request);
    String receive();
}

// 具体实现:支付
class WechatChannel implements PaymentChannel {
    public boolean send(String request) {
        // 调用 HTTP API
        return true;
    }
    public String receive() {
        // 解析回调
        return "result";
    }
}

// 抽象化:支付业务
abstract class Payment {
    protected PaymentChannel channel;
    
    protected Payment(PaymentChannel channel) {
        this.channel = channel;
    }
    
    abstract boolean pay(String orderId, double amount);
    abstract boolean refund(String orderId);
}

// 精确抽象:普通支付
class NormalPayment extends Payment {
    public NormalPayment(PaymentChannel channel) {
        super(channel);
    }
    
    boolean pay(String orderId, double amount) {
        String req = buildPayRequest(orderId, amount);
        return channel.send(req);
    }
    
    boolean refund(String orderId) {
        String req = buildRefundRequest(orderId);
        return channel.send(req);
    }
}

你看,新增一个「支付宝渠道」,只需要加一个 AlipayChannel implements PaymentChannel。新增一个「签约支付」业务,只需要加一个 ContractPayment extends Payment。互不干扰。

我曾经踩过的坑:桥接模式虽然好,但别滥用。如果你的抽象和实现只有一种变化可能,或者变化频率极低,用桥接反而增加复杂度。我见过有人把只有一种实现方式的接口也拆成桥接,结果代码绕了三层,调试时追得头晕。记住:模式是为解决问题服务的,不是为了炫技。

桥接模式的结构图

下面这张图,把桥接模式的核心结构画出来了。你看一眼就能明白:抽象和实现之间,就是通过一条「桥」连接的。

桥接模式结构图 Abstraction - implementor: Implementor + operation() RefinedAbstraction + operation() Implementor <<interface>> + operationImpl() ConcreteImplA + operationImpl() ConcreteImplB + operationImpl() 桥接 关键说明 1. Abstraction 持有 Implementor 的引用,通过组合建立桥接 2. 抽象维度(Abstraction 系列)和实现维度(Implementor 系列)独立变化 3. 新增一种抽象或一种实现,都不需要修改对方 4. 变化维度数从 M×N 降为 M+N,复杂度大幅降低

什么时候用桥接模式?

我总结了几条判断标准,你可以对照着看:

  1. 两个维度独立变化:一个类的变化原因有两个以上,且互不依赖
  2. 避免类爆炸:继承会导致子类数量呈乘积增长
  3. 运行时切换:需要在运行时动态选择实现方式
  4. 实现细节对抽象透明:抽象层不需要知道底层怎么实现的

接口适配的场景,几乎都满足这些条件。你想想看,对接第三方系统时,对方的接口协议、通信方式、数据格式,哪个不是独立变化的?今天对接支付,明天对接支付宝,后天可能还要对接 Stripe。桥接模式就是为这种场景量身定做的。

一个小建议:刚开始用桥接模式时,别急着把代码全拆了。先找出最常变化的两个维度,从它们开始拆。拆完之后,你会发现新增功能时,改动的代码量明显减少。那种「改一处,全盘皆动」的恐惧感,会慢慢消失。