本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是桥接模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:
- 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
- 模式结构:包含模式的角色定义及调用关系以及其模版代码
- 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
- 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
- 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
- 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构
接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构
模式档案
在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车等。如果用桥接模式就能很好地解决这些问题
模式定义:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
解决什么问题:一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。通过组合关系来替代继承关系,避免继承层次的指数级爆炸
优点:抽象与实现分离,扩展能力强;符合开闭原则;符合合成复用原则;其实现细节对客户透明
缺点:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度
使用场景:当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时
桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。因为父类拥有的方法,子类也会继承得到,无论子类需不需要,这说明继承具备强侵入性(父类代码侵入子类),同时会导致子类臃肿
模式结构
桥接(Bridge)模式包含以下四种主要角色。
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现
整体结构如下:
模式实现
依据桥接模式的关系图定义四类角色代码如下:
实现化角色
//实现化角色 interface Implementor { public void OperationImpl(); }
具体实现化角色
//具体实现化角色 class ConcreteImplementorA implements Implementor { public void OperationImpl() { System.out.println("具体实现化(Concrete Implementor)角色被访问"); } }
抽象化角色
//抽象化角色 abstract class Abstraction { protected Implementor imple; protected Abstraction(Implementor imple) { this.imple = imple; } public abstract void Operation(); }
扩展抽象化角色
//扩展抽象化角色 class RefinedAbstraction extends Abstraction { protected RefinedAbstraction(Implementor imple) { super(imple); } public void Operation() { System.out.println("扩展抽象化(Refined Abstraction)角色被访问"); imple.OperationImpl(); } }
客户端调用如下:
public class BridgeTest { public static void main(String[] args) { Implementor imple = new ConcreteImplementorA(); Abstraction abs = new RefinedAbstraction(imple); abs.Operation(); } }
打印结果如下:
扩展抽象化(Refined Abstraction)角色被访问 具体实现化(Concrete Implementor)角色被访问
模式实践
我们来看两个实践的例子:JDBC源码分析以及消息系统构建
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
就可以了,这里我们需要继续查看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 类中。
DriverManager 类是干什么用的。具体的代码如下所示。当我们把具体的 Driver 实现类(比如,com.mysql.jdbc.Driver
)注册到 DriverManager 之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver 实现类来执行。而 Driver 实现类都实现了相同的接口(java.sql.Driver ),这也是可以灵活切换 Driver 的原因
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 本身就相当于抽象。注意,这里所说的抽象,指的并非抽象类或接口,而是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于实现。注意,这里所说的实现,也并非指接口的实现类,而是跟具体数据库相关的一套类库,JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行,例如prepare方法就是获取到连接后执行。
protected PreparedStatement prepare() throws SQLException { this.conn = this.connect(); try { Map var1 = this.getTypeMap(); if (var1 != null) { this.conn.setTypeMap(var1); } this.ps = this.conn.prepareStatement(this.getCommand(), 1004, 1008); } catch (SQLException var2) { System.err.println(this.resBundle.handleGetObject("jdbcrowsetimpl.prepare").toString() + var2.getLocalizedMessage()); if (this.ps != null) { this.ps.close(); } if (this.conn != null) { this.conn.close(); } throw new SQLException(var2.getMessage()); } return this.ps; }
而该连接就是通过driver获取的。
public interface Driver { Connection connect(String url, java.util.Properties info) throws SQLException; //... }
设计一个可扩展的监控告警方案
我们想设计一套监控告警方案:根据不同的告警规则,触发不同类型的告警。
- 告警支持多种通知渠道,包括:邮件、短信、微信、自动语音电话。
- 告警通知的紧急程度有多种类型,包括:SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要)。
不同的紧急程度对应不同的通知渠道。比如,SERVE(严重)级别的消息会通过“自动语音电话”告知相关人员,我们先来看最简单、最直接的一种实现方式
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 分支逻辑。实际上,如果每个分支中的代码都不复杂,后期也没有无限膨胀的可能(增加更多 if-else 分支判断),那这样的设计问题并不大,没必要非得一定要摒弃 if-else 分支逻辑。不过Notification 的代码显然不符合这个条件。因为每个 if-else 分支中的代码逻辑都比较复杂,发送通知的所有逻辑都扎堆在 Notification 类中。我们知道,类的代码越多,就越难读懂,越难修改,维护的成本也就越高。很多设计模式都是试图将庞大的类拆分成更细小的类,然后再通过某种更合理的结构组装在一起
桥接模式降低双维度耦合度
针对 Notification 的代码,我们将不同渠道的发送逻辑剥离出来,形成独立的消息发送类(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代码结构类似,所以省略... }
这样当我们有了新的紧急程度或者通知方式,都可以直接扩展一个通知类或消息发送类即可,不需要修改任何代码。
模式对比
这里我们对比下已经学习过的四种结构型设计模式:代理、桥接、装饰器、适配器
代理、桥接、装饰器、适配器四种设计模式
代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别
- 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
- 桥接模式:桥接模式的目的是将抽象部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
- 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
- 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口
其实我觉得从代码结构上去区分这四种结构型模式非常容易搞混,也大可不必,因为它们的结构非常相似,反而是从要解决的问题、应用场景出发,首先对问题有宏观认知,然后再去思考这个场景适配哪种模式,然后再去关心代码结构是个比较不错的思考方式。
总结一下
其实我觉得从代码结构上去区分代理、桥接、装饰器、适配器这四种结构型模式非常容易搞混(甚至退化的装饰器结构能和代理模式玩去一样)也大可不必,因为它们的结构非常相似,反而从要解决的问题、应用场景角度出发,先对问题有个宏观认知,然后再去思考这个场景适配哪种模式,最后再去关心代码结构应该怎么写,才是个比较不错的思考方式。