设计模式基础(六)——桥接模式
桥接模式(Bridge Design Pattern),也是一种结构型模式。桥接模式的代码实现非常简单,但是理解起来稍微有点难度,并且应用场景也比较局限。相对于代理模式来说,桥接模式在实际的项目中并没有那么常用,所以只需要简单了解即可。
一、基本原理
GoF 对桥接模式的定义非常简短:“Decouple an abstraction from its implementation so that the two can vary independently”。翻译成中文就是:“将抽象和实现解耦,让它们可以独立变化”。
1.1 JDBC示例
估计没几个人能看懂是什么意思。所以,我们通过 JDBC 驱动的例子来解释一下。JDBC 驱动是桥接模式的经典应用。我们先来看一下,如何利用 JDBC 驱动来查询数据库。
Class.forName("com.mysql.jdbc.Driver");//加载及注册JDBC驱动程序
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
rs.getString(1);
rs.getInt(2);
}
如果我们想要把 MySQL 数据库换成 Oracle 数据库,只需要把第一行代码中的 com.mysql.jdbc.Driver 换成 oracle.jdbc.driver.OracleDriver 就可以了,获取直接从配置文件中加载,这样在切换数据库的时候,我们都不需要修改代码,只需要修改配置文件就可以了。
那么JDBC底层到底是如何实现的呢?我们首先必须要搞明白一点,JDBC其实是一整套Java数据库访问api,有点类似于servlet。JDK自身定义了数据库访问的API和一整套操作逻辑,而数据库厂商们要做的就是负责实现相应的驱动类:

比如,下面是MySQL实现的Driver驱动,按照JDBC规范实现java.sql.Driver接口,并注册到java.sql.DriverManager中:
package com.mysql.jdbc;
import java.sql.SQLException;
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!");
}
}
/**
* Construct a new driver and register it with DriverManager
* @throws SQLException if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
java.sql.DriverManager 负责执行所有对 JDBC 接口的调用:
public class DriverManager {
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();
//...
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
//...
public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
if (driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver));
} else {
throw new NullPointerException();
}
}
public static Connection getConnection(String url, String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
//...
}
不知道看到这里,读者有没有明白些了,桥接模式的定义是“将抽象和实现解耦,让它们可以独立变化”。在 JDBC 这个例子中,JDBC 规范和自身的相应操作,就相当于“抽象”,具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。
二、使用示例
我们再来通过一个示例更好的理解下桥接模式。假设,我们需要做一个API 接口监控告警框架,根据不同的紧急程度,触发不同通知渠道的告警。
2.1 使用桥接模式前
首先,告警的紧急程度有多种类型,包括:SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要):
public enum NotificationEmergencyLevel {
SEVERE, URGENCY, NORMAL, TRIVIAL
}
然后,不同的紧急程度对应着不同的通知渠道:
public class Notification {
private List<String> emailAddresses;
private List<String> telephones;
private List<String> wechatIds;
public Notification() {}
public void setEmailAddress(List<String> emailAddress) {
this.emailAddresses = emailAddress;
}
public void setTelephones(List<String> telephones) {
this.telephones = telephones;
}
public void setWechatIds(List<String> wechatIds) {
this.wechatIds = wechatIds;
}
public void notify(NotificationEmergencyLevel level, String message) {
if (level.equals(NotificationEmergencyLevel.SEVERE)) {
//...自动语音电话
} else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
//...发微信
} else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
//...发邮件
} else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
//...发邮件
}
}
}
我们可以像下面这样来使用这个框架:
public class ErrorAlertHandler extends AlertHandler {
@Override
public void check(ApiStatInfo apiStatInfo) {
if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
// 触发告警
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
}
}
上面这种实现方式,有一个最明显的问题,那就是有很多 if-else 分支逻辑,即发送通知的所有逻辑都扎堆在 Notification 类中。另外,如果我们想要改动紧急程度和通知渠道的关系,也必须显示修改Notification。
2.2 使用桥接模式后
我们可以将不同渠道的通知逻辑剥离出来,形成独立的消息发送接口MsgSender :
public interface MsgSender {
void send(String message);
}
然后委托给Notification,由Notification定义通用的消息发送规范:
public abstract class Notification {
protected MsgSender msgSender;
public Notification(MsgSender msgSender) {
this.msgSender = msgSender;
}
public abstract void notify(String message);
}
这样,Notification就相当于桥接模式中的“抽象”,MsgSender的各个实现类就相当于桥接模式中的“实现”:
public class TelephoneMsgSender implements MsgSender {
private List<String> telephones;
public TelephoneMsgSender(List<String> telephones) {
this.telephones = telephones;
}
@Override
public void send(String message) {
//...
}
}
public class EmailMsgSender implements MsgSender {
// 与TelephoneMsgSender代码结构类似,所以省略...
}
public class WechatMsgSender implements MsgSender {
// 与TelephoneMsgSender代码结构类似,所以省略...
}
两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。所谓任意组合,就是不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死的,我们可以动态地去指定(比如,通过读取配置来获取对应关系):
public class UrgencyNotification extends Notification {
// 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {
// 与SevereNotification代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {
// 与SevereNotification代码结构类似,所以省略...
}
三、总结
桥接模式的代码实现并不复杂,关键是弄懂定义中“抽象”和“实现”两个概念:
- 定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成;
- 定义中的“实现”,也并非“接口的实现类”,而是一套独立的“类库”。
桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起。
感谢赞赏~