桥接模式:抽象与实现分离、JDBC中的桥接模式

大家好,今天我们来聊一个非常实用的设计模式——桥接模式。

说实话,我刚接触这个模式的时候,觉得它有点绕。什么叫「抽象与实现分离」?抽象和实现本来不就是在一起的吗?后来我在一个项目中踩了坑,才真正理解了它的价值。

嗯,咱们慢慢聊。

什么是桥接模式?

桥接模式,说白了就是把抽象部分和实现部分拆开,让它们可以独立变化。

你想想看,如果一个类既要处理业务逻辑,又要处理底层实现,那它就会变得很臃肿。而且一旦底层变了,整个类都得改。

桥接模式就是来解决这个问题的。它通过组合的方式,把抽象和实现连接起来——就像一座桥。

核心思想:用组合替代继承,让抽象和实现可以各自独立扩展。

一个生活中的例子

我记得有一次给团队讲这个模式,用了「遥控器」的例子。

遥控器是抽象,电视机是实现。你换一个遥控器,不需要换电视机;你换一台电视机,遥控器照样能用。它们之间通过「红外信号」这座桥来通信。

这就是桥接模式。

模式结构

桥接模式有四个角色:

  • Abstraction(抽象类):定义抽象接口,持有Implementor的引用
  • RefinedAbstraction(扩展抽象类):对抽象类进行扩展
  • Implementor(实现接口):定义实现类的接口
  • ConcreteImplementor(具体实现类):实现Implementor接口

我习惯把Abstraction比作「遥控器」,Implementor比作「电视机」。这样一想,结构就清晰了。

代码示例

咱们来看一个实际的例子。假设我们要开发一个消息发送系统,支持普通消息和加急消息,发送方式可以是邮件或短信。

如果用继承,你会得到4个类:普通邮件、普通短信、加急邮件、加急短信。如果再加一种消息类型或发送方式,类会爆炸。

用桥接模式就优雅多了:

// 实现接口
public interface MessageSender {
    void send(String message);
}

// 具体实现
public class EmailSender implements MessageSender {
    @Override
    public void send(String message) {
        System.out.println("发送邮件: " + message);
    }
}

public class SmsSender implements MessageSender {
    @Override
    public void send(String message) {
        System.out.println("发送短信: " + message);
    }
}

// 抽象类
public abstract class Message {
    protected MessageSender sender;
    
    public Message(MessageSender sender) {
        this.sender = sender;
    }
    
    public abstract void send(String message);
}

// 扩展抽象类
public class NormalMessage extends Message {
    public NormalMessage(MessageSender sender) {
        super(sender);
    }
    
    @Override
    public void send(String message) {
        sender.send("[普通]" + message);
    }
}

public class UrgentMessage extends Message {
    public UrgentMessage(MessageSender sender) {
        super(sender);
    }
    
    @Override
    public void send(String message) {
        sender.send("[加急]" + message);
    }
}

使用的时候:

Message msg = new UrgentMessage(new EmailSender());
msg.send("系统故障,请立即处理!");

你看,现在要加一种「微信发送」,只需要加一个WechatSender类。要加一种「定时消息」,只需要加一个TimedMessage类。互不影响。

我的习惯:在设计初期,如果发现一个维度可能有两个以上的变化方向,我就会考虑桥接模式。比如消息的类型和发送方式,就是两个独立的变化维度。

JDBC中的桥接模式

说到桥接模式,最经典的例子就是JDBC。

我刚开始学Java的时候,一直没搞明白:为什么JDBC要搞一个DriverManager,还要注册驱动?后来理解了桥接模式,才恍然大悟。

JDBC中:

  • 抽象部分:Connection、Statement、ResultSet等接口
  • 实现部分:MySQL驱动、Oracle驱动、PostgreSQL驱动等
  • :DriverManager和Driver接口

你写代码的时候,用的是统一的JDBC接口。底层用的是MySQL还是Oracle,完全由驱动决定。这就是抽象与实现分离。

// 你写的代码
Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");

// 底层实际调用的是MySQL或Oracle的实现
// 但你完全不需要关心

为什么会这样?因为DriverManager就是那座桥。它把JDBC接口(抽象)和具体数据库驱动(实现)连接了起来。

关键点:JDBC的接口是稳定的,数据库驱动是可以替换的。这就是桥接模式的价值——让抽象和实现各自独立演化。

桥接模式的核心逻辑

我画了一张图,帮你理解桥接模式的结构:

桥接模式结构图 抽象部分 (Abstraction) - 持有 Implementor 引用 - 定义业务方法 - 委派给 Implementor 执行 RefinedAbstraction 扩展抽象类的功能 实现部分 (Implementor) - 定义实现接口 - 提供底层操作 - 独立于抽象变化 ConcreteImpl A 具体实现A ConcreteImpl B 具体实现B JDBC 中的桥接模式示例 抽象部分:Connection, Statement, ResultSet (JDBC标准接口) 实现部分:MySQL驱动, Oracle驱动, PostgreSQL驱动 (具体数据库实现)

什么时候用桥接模式?

我个人总结了几个场景:

  • 一个类有两个独立变化的维度:比如消息的类型和发送方式
  • 不想在抽象和实现之间建立固定的绑定关系:比如JDBC,你不想写死用哪个数据库
  • 抽象和实现都需要独立扩展:比如UI组件和操作系统,Windows和Mac的按钮实现不同,但按钮的抽象行为是一样的

避坑指南:我曾经在一个项目中过度使用桥接模式,把本来只有一个变化维度的类也拆成了抽象和实现。结果代码变得很复杂,维护成本反而高了。

记住:桥接模式是为了解决「多维度变化」的问题。如果只有一个维度在变,用策略模式或者简单继承就够了。

桥接模式 vs 适配器模式

这两个模式容易搞混。我简单说一下区别:

对比维度 桥接模式 适配器模式
设计目的 分离抽象与实现,让它们独立变化 让不兼容的接口能够协同工作
使用时机 设计阶段,提前规划 后期维护,适配已有代码
变化方向 抽象和实现各自变化 适配已有的接口
典型例子 JDBC、GUI组件 电源适配器、日志框架适配

说白了,桥接模式是「主动设计」,适配器模式是「被动适配」。一个是在写代码之前就想好的,一个是遇到问题后补救的。

总结

桥接模式的核心就一句话:用组合代替继承,让抽象和实现各自独立

我在项目中用过很多次桥接模式,最爽的一次是做一个多平台的消息推送系统。iOS和Android的推送实现完全不同,但业务层的消息逻辑是一样的。用桥接模式,业务层和推送层完全解耦,后来加了个华为推送,只加了一个实现类,业务层一行代码没改。

嗯,这就是桥接模式的魅力。

最后一个小建议:下次你写代码的时候,如果发现一个类里有「if-else」判断不同平台或不同实现,不妨想想——这里是不是可以用桥接模式?

公众号:蓝海资料掘金营,微信 deep3321