桥接模式:抽象与实现分离、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的接口是稳定的,数据库驱动是可以替换的。这就是桥接模式的价值——让抽象和实现各自独立演化。
桥接模式的核心逻辑
我画了一张图,帮你理解桥接模式的结构:
什么时候用桥接模式?
我个人总结了几个场景:
- 一个类有两个独立变化的维度:比如消息的类型和发送方式
- 不想在抽象和实现之间建立固定的绑定关系:比如JDBC,你不想写死用哪个数据库
- 抽象和实现都需要独立扩展:比如UI组件和操作系统,Windows和Mac的按钮实现不同,但按钮的抽象行为是一样的
避坑指南:我曾经在一个项目中过度使用桥接模式,把本来只有一个变化维度的类也拆成了抽象和实现。结果代码变得很复杂,维护成本反而高了。
记住:桥接模式是为了解决「多维度变化」的问题。如果只有一个维度在变,用策略模式或者简单继承就够了。
桥接模式 vs 适配器模式
这两个模式容易搞混。我简单说一下区别:
| 对比维度 | 桥接模式 | 适配器模式 |
|---|---|---|
| 设计目的 | 分离抽象与实现,让它们独立变化 | 让不兼容的接口能够协同工作 |
| 使用时机 | 设计阶段,提前规划 | 后期维护,适配已有代码 |
| 变化方向 | 抽象和实现各自变化 | 适配已有的接口 |
| 典型例子 | JDBC、GUI组件 | 电源适配器、日志框架适配 |
说白了,桥接模式是「主动设计」,适配器模式是「被动适配」。一个是在写代码之前就想好的,一个是遇到问题后补救的。
总结
桥接模式的核心就一句话:用组合代替继承,让抽象和实现各自独立。
我在项目中用过很多次桥接模式,最爽的一次是做一个多平台的消息推送系统。iOS和Android的推送实现完全不同,但业务层的消息逻辑是一样的。用桥接模式,业务层和推送层完全解耦,后来加了个华为推送,只加了一个实现类,业务层一行代码没改。
嗯,这就是桥接模式的魅力。
最后一个小建议:下次你写代码的时候,如果发现一个类里有「if-else」判断不同平台或不同实现,不妨想想——这里是不是可以用桥接模式?