概述
今天学习另外一种结构型模式:桥接模式。桥接模式的代码实现非常简单,但是理解起来稍微优点难度,并且应用场景也比较局限,所以,相对于代理模式来说,桥接模式在实际的项目中并没有那么常用,你只需要简单了解即可,见到了能认识就可以了。
桥接模式的原理解析
桥接模式,也叫做 “桥梁模式”,它是 23 种设计模式中最难理解的模式之一了。对于这个模式有两个不同的理解方式。
- 在 GoF 的《设计模式》一书中,桥接模式是这么定义的:“Decouple an abstraction from its implementation so that the two can vary independently。” 翻译成中文就是:“
MsgSender
”。 - 还有另一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(多多个)维度可以独立进行扩展。” 通过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于,之前讲过的 “组合优于继承” 设计原则,所以,这里就不多做解释了。
我们重点看下 GoF 的理解方式。GoF 给出的定义非常简短,单凭这一句话,估计没几个人能看懂事什么意思。所以,我们通过 JDBC 驱动的例子来解释下。JDBC 驱动是桥接模式的经典应用。我们来看一下,如何利用 JDBC 驱动来查询数据库。具体的代码如下所示。
Class.forName("com.mysql.jdbc.Driver");
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
就可以了。当然,也有更灵活的实现方式,我们可以把需要加载的 Driver
类写到配置文件中,当程序启动的时候,自动从配置文件中加兹安,这样在切换数据的时候,我们都不需要修改代码,只需要修改配置文件就可以了。
不管是改代码还是改配置,在项目中,从一个数据库切换到另一个种数据库,都只需要改动很少的代码,或者完全不需要改动代码,那如此优雅的数据库切换是如何实现的呢?
源码之下无秘密。要弄清楚这个问题,我们先从 com.mysql.jdbc.Driver
这个类的代码看起。下面是少部分相关代码,放到了这里,你可以看一下。
package com.mysql.cj.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()
}
}
结合 com.mysql.jdbc.Driver
的代码实现,可以发现,当执行 Class.forName("com.mysql.jdbc.Driver")
这条语句时,实际上是做了两件事情。
- 第一件事情,是要求 JVM 查找并加载指定的
Driver
类, - 第二件事情,是执行改类的静态代码,也就是将 MySQL
Driver
注册到DriverManager
类中。
现在,再看下, DriverManager
类是干什么的。具体代码如下所示。当我们把具体的 Driver
实现类(比如 com.mysql.jdbc.Driver
)注册到 DriverManager
之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver
实现类来执行。而 Driver
实现类都实现了相同的接口(java.sql.Driver
),这也是可以灵活切换 Driver
的原因。
public class DriverManager {
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
// ...
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
@CallerSensitive
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
来执行。
下面画一张图帮你你理解。
桥接模式的应用举例
在 《设计原则 - 2.开闭原则》中,我们讲过一个 API 接口告警的例子:根据不同的告警规则,触发不同类型的告警。告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。通知的紧急程度有多种类型,包括:SERVER(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。不同的紧急程度对应不同的通知渠道。比如,SERVER(严重)基本的消息会通过 “自动语音电话” 告知相关人员。
在当时的代码实现中,关于发送告警信息那部分代码,我们只给出了粗略的设计,现在我们来一块实现下。先来看最简单、最直接的一种实现方式。
public enum NotificationEmergencyLevel {
SERVER, URGENCY, NORMAL, TRIVIAL
}
public class Notification {
private List<String> emailAddress;
private List<String> telephones;
private List<String> wechatIds;
public Notification() {}
public void setEmailAddress(List<String> emailAddress) {
this.emailAddress = 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.SERVER)) {
// 自动语音电话...
} else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
// 自动发微信...
} else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
// 自动发邮件...
} else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
// 自动发邮件...
}
}
}
// API告警监控的例子中,通过如下方式来使用Notification类
public class ErrorAlertHandler extends AlertHandler {
public ErrorAlertHandler(AlertRule rule, Notification notification) {
super(rule, notification);
}
@Override
public void check(ApiStatInfo apiStatInfo) {
if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SERVER, "...");
}
}
}
Notification
类的代码实现有一个最明显的问题,那就是有很多 if-else 分支逻辑。实际上,如果每个分支的代码不复杂,后期也没有无线膨胀的可能(增加更多的 if-else 分支判断),那这样的设计问题并不大,没有必要一定要摒弃 if-else 分支逻辑。
不过,Notification
的代码显然不符合这个条件。因为每个 if-else 分支中的代码逻辑都比较复杂,发送通知的所有逻辑都扎堆在 Notification
类中。我们知道,类的代码越多,就越难读懂,越难修改,维护的成本也就越高。很多设计模式都是试图将庞大的类拆分成更细小的类,然后再通过某种更合理的结构组合在一起。
针对 Notification
代码,可以将不同渠道的发送逻辑剥离出来,形成独立的消息发送类(MsgSender
相关类)。其中 Notification
类相当于抽象类,MsgSender
相当于实现类,两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。所谓任何组合的意思是,不同紧急程度的消息和发送渠道的对应关系,不是在代码中固定写死,可以动态地去指定(比如,通过读取配置来获取对应的关系)。
按照这个设计思路,对代码进行重构。
public interface MsgSender {
void send(String msg);
}
public class TelephoneMsgSender implements MsgSender {
private List<String> telephones;
public TelephoneMsgSender(List<String> telephones) {
this.telephones = telephones;
}
@Override
public void send(String msg) {
// 自动语音电话...
}
}
public class EmailMsgSender implements MsgSender {
private List<String> emailAddress;
public EmailMsgSender(List<String> emailAddress) {
this.emailAddress = emailAddress;
}
@Override
public void send(String msg) {
// 自动发邮件...
}
}
public class WechatMsgSender implements MsgSender {
private List<String> wechatIds;
public WechatMsgSender(List<String> wechatIds) {
this.wechatIds = wechatIds;
}
@Override
public void send(String msg) {
// 自动发微信...
}
}
public abstract class Notification {
protected MsgSender msgSender;
public Notification(MsgSender msgSender) {
this.msgSender = msgSender;
}
public abstract void notify(String message);
}
public class ServerNotification extends Notification {
public ServerNotification(MsgSender msgSender) {
super(msgSender);
}
@Override
public void notify(String message) {
msgSender.send(message);
}
}
public class UrgencyNotification extends Notification {
public UrgencyNotification(MsgSender msgSender) {
super(msgSender);
}
@Override
public void notify(String message) {
msgSender.send(message);
}
}
public class NormalNotification extends Notification {
public NormalNotification(MsgSender msgSender) {
super(msgSender);
}
@Override
public void notify(String message) {
msgSender.send(message);
}
}
public class TrivialNotification extends Notification {
public TrivialNotification(MsgSender msgSender) {
super(msgSender);
}
@Override
public void notify(String message) {
msgSender.send(message);
}
}
总结
桥接模式的原理比较难理解,但代码实现相对简单些。
对于这个模式有两种不同的理解方式。
- GoF 的《设计模式》中,桥接模式被定义为:“将抽象和实现解耦,让它们可以独立变化。”
- 在其他书籍和资料中,还有另一种更加简单的理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”
对于第一种 GoF 的理解方式,弄懂定义中的 “抽象” 和 “实现” 两个概念,是理解它的关键。
- 定义中的 “抽象”,指的并非是抽象类或接口,而是被抽象出来的一套 “类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的 “实现” 来完成。
- 而定义中的 “实现” 也并非接口的实现类,而是一套独立的 “类库”。
“抽象” 和 “实现” 独立开发,通过对象之间的组合关系,组装在一起。
对于第二种理解方式,它非常类似于我们之前讲过的 “组合优于继承” 设计原则,通过组合关系来替代继承关系,避免继承层次的指数级爆炸。