写在前
Decouple an abstraction from its implementation so that the two can vary independently.
解耦抽象和实现,使得两者可以独立的变化。
桥接模式遵循了里式替换原则和依赖倒置原则,最终实现了开闭原则!
桥接模式 的核心在于 解耦抽象和实现,类似于多重继承方案,但是多重继承方案往往违背了类得单一职责原则,其复用性比较差,桥接模式 是比多重继承更好的替代方案。
注:此处的 抽象 并不是指 抽象类或 接口这种高层概念,实现 也不是 继承 或 接口实现 。抽象 与 实现 其实指的是两种独立变化的维度。其中,抽象 包含 实现,因此,一个抽象类的变化可能涉及到多种维度的变化导致的。
主要解决的问题:
当一个类内部具备两种或多种变化维度时,使用 桥接模式 可以解耦这些变化的维度,使高层代码架构稳定。
1.应用举例1
桥接模式的一个经典应用就是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
- 把需要加载的 Driver 类写到配置文件中,当程序启动的时候,自动从配置文件中加载,这样在切换数据库的时候,我们都不需要修改代码,只需要修改配置文件就可以了。
不管是改代码还是改配置,在项目中,从一个数据库切换到另一种数据库,都只需要改动很少的代码,或者完全不需要改动代码,那如此优雅的数据库切换是如何实现的呢?要弄清楚这个问题,我们先从 com.mysql.jdbc.Driver 这个类的代码看起。
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() } }
结合 com.mysql.jdbc.Driver 的代码实现,我们可以发现,当执行 Class.forName(“com.mysql.jdbc.Driver”) 这条语句的时候,实际上是做了两件事情:
- 第一件事情是要求 JVM 查找并加载指定的 Driver 类。
- 第二件事情是执行该类的静态代码,也就是将 MySQL Driver 注册到 DriverManager 类中。
当我们把具体的 Driver 实现类(比如,com.mysql.jdbc.Driver)注册到 DriverManager 之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver 实现类来执行。而 Driver 实现类都实现了相同的接口(java.sql.Driver),这也是可以灵活切换 Driver 的原因。下面看下 DriverManager 的具体代码。
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 本身就相当于“抽象”。注意,这里所说的“抽象”,指的并非“抽象类”或“接口”,而(JDBC)是跟具体的数据库无关的、被抽象出来的一套“类库”,是抽象的。
- 具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。注意,这里所说的“实现”,也并非指“接口的实现类”,而(具体的Driver)是跟具体数据库相关的一套“类库,是实现的”。
JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。
2.应用举例2
下面给出API接口监控告警的例子,关于发送告警信息那部分代码,给出简单的实现:
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)) { //...发邮件 } } } //在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.SEVERE, "..."); } } }
上述存在的问题:Notification最明显的问题,含有很多if-else分支逻辑。
解决方案:将不同渠道发送逻辑剥离出来,形成独立的消息发送类(MsgSender相关类),这样Notification类相当于抽象,MsgSender相关类相当于实现,两者可以进行独立开发,通过组合关系(也就是桥梁)任意组合在一起。代码如下:
public interface MsgSender { void send(String message); } 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 abstract class Notification { protected MsgSender msgSender; public Notification(MsgSender msgSender) { this.msgSender = msgSender; } public abstract void notify(String message); } public class SevereNotification extends Notification { public SevereNotification(MsgSender msgSender) { super(msgSender); } @Override public void notify(String message) { msgSender.send(message); } } public class UrgencyNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... } public class NormalNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... } public class TrivialNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... }
3.总结与补充
(1)优缺点
优点:
- 抽象与实现分离:这是桥接模式的主要特点,也是避免使用继承主要原因,这使得两者的变化不会对另一方产生影响。
- 优秀的扩展能力:桥接模式的出现就是为了解决多个独立变化的维度的耦合,其高层模块聚合关系已经确定(稳定)。因此,无论是抽象变化还是现实变化,只要对其进行扩展即可,高层代码无需任何更改即可接收扩展。高层代码依赖抽象,遵循依赖倒置原则。
缺点:由于聚合关系建立在抽象层,要求开发者对抽象进行设计与编程,因此增加了系统的理解与设计难度。
(2)主要应用场景:
- 一个类存在两个(或多个)独立变化的维度,且这两个维度都需要进行扩展;
- 不希望或不适合使用继承的场景(常用的场景就是为了替换继承);
注:继承具有抽象、封装、多态等,父类封装共性,子类实现特性。但由于这种强侵入性(父类代码侵入子类代码,导致子类代码臃肿),所以我们推荐多使用组合,少使用继承的设计模式!具体使用还是要考虑现实场景!!
4.参考
极客时间《设计模式之美》 ----- 王争