- 参考书籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》
- Bridge pattern - Wiki
前言
桥接模式是一种提及频率很高, 应用频率较少的设计模式。 桥接模式之所以被频繁提及, 是因为它的设计意图提到了“解耦”, 然而它的解耦方式却常常被很多人误解。
设计模式用前须知
- 设计模式种一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
- 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句话
往往只对框架/ 工具包的设计才有真正的意义
。框架和工具包存在的意义,就是为了让其他的程序员使用,在其基础上实现功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
- 对于应用程序的编写者, 从理论上来说, 所有应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。
桥接模式 ( Bridge )
-
设计意图
- GOF: “ 解耦抽象与实现, 使得二者可以独立地变化。”
- 80% 的程序员听到这句话, 第一反应都是: 一个接口(抽象类)可以对应多个实现类, 通过接口可以解耦抽象与实现。
- 有趣的是,
第一反应是错误的
。 原因是设计意图的后半句话没有被满足: “二者可以独立地变化”当一个接口被定义好以后,实现类的确可以随意变化, 但是接口可以独立于实现类随意变化吗?
如果你向接口中添加一个方法, 那之前所有实现了该接口的类是否还满足该接口?- 答案显然是 “不”。
- GOF举例 :
- 考虑一个用于编写图形界面的工具包/类库, 假设该工具包支持编写可移植的跨平台界面, 其中定义了一个窗口的抽象(接口/抽象类)Window。 为了支持X平台和PM平台, 需要两个实现类 XWindow, PMWindow.
- 到目前为止, 一切都很好。 可是当你试图想要扩展Window的抽象时, 就会发现问题。 假设我们要定义一个抽象类 IconWindow, 专用于描述 “图标窗口” 所需要实现的方法。 此时由于IconWindow也需要支持X平台和PM平台, 所以就需要再实现XIConWindow, PMIconWindow。
- 从上述结构可以看出,
每增加一种抽象类abcWindow, 都需要额外实现两个平台的实现类 XabcWindow, PMabcWindow
。 这个问题的深层次原因是 : 接口和其实现类是存在耦合关系的, 每当你想要改变接口定义的时候,实现类必须也有相应的变化。
- GOF: “ 解耦抽象与实现, 使得二者可以独立地变化。”
-
解决方案
图例说明
这种关系在 java 语言中, 可以按照如下形式实现(不止这一种实现形式)
abstract class A {
private B b;
}
注意: A, B 的类型比较灵活, 可以是 class, abstract class , interface 的任意一种类型, 使用哪种完全看需求
这种关系在 java 语言中, 可以按照如下形式实现
class A extends B{
}
或
class A implements B{
}
桥接模式解析
- 注意点1:Window 定义的方法是有方法体的, Window 中定义的绘制矩形 DrawRect 操作, 都是通过调用 impl 的方法实现的。 (这并不意味着 Window 只能是一个抽象类, Java8 是支持在接口中定义方法体的哟)
- 注意点2: WindowImpl 也是一个抽象类( 接口), 并非一个具体的实现类
- 注意点3: Window 的子抽象类(子接口)中定义的新方法,都是通过调用 WindowImpl 中的方法或调用 Window 中的方法间接定义的。
- 这一点是最容易被忽略的: 如果 Window 的子接口中,直接增加一个新的方法 DrawCircle(), 搭在Window 和 WindowImpl 之间“桥” 就失效了。 桥右端的实现类中, 是无法间接地实现DrawCircle 这个新增的方法的。
- 桥接模式的设计意图虽然是让 “抽象部分“和“实现部分“ 能够独立变化,但经过上述分析不难发现, 抽象接口的变化还是有所限制的, 并不能随意变化。
- 下面这张图更加形象地描述了桥接模式中重定义的抽象类(RedefinedAbstraction)中的 Composite Operations 对于基础抽象类中(Abstraction)中的 Abstract Operations 的依赖关系
- 桥接模式泛化结构图
桥接模式与抽象工厂
- 上述的例子里, 提到了跨平台的问题, 回想一下可以发现, 抽象工厂模式中所举的例子, 出发点也是跨平台, 那两者这件是否有所关联? 答案是肯定的。
- 还是以之前的例子进行说明, 注意到基础抽象类 Window 中持有了一个 WindowImp 的引用imp , 如果 Window 可以预先确定要支持的只有X平台和PM平台, 就可以在 Window 构造器中通过参数来指定实例化哪一种 WindowImpl.
public Window ( String type )
{
switch(type){
case "X"
this.imp = new XWindowImp();
break;
case "PW"
this.imp = new PMWindowImpl();
break;
}
}
如果Window中无法预先确定有哪些实现类, 想给用户提供扩展支持平台的机会, 那么就可以利用抽象工厂模式来实例化impl 对象
public Window ( AbstractFactory factory )
{
this.impl = factory.createWindowImpl()
}
通过传入用户自定义的新的Factory实现类 , 就可以扩展Window所支持的平台。
- 综上, 抽象工厂模式可以用来搭建或者配置桥接模式中的 “桥梁” 。
桥接模式的真实应用案例 JDBC
jdbc 作为 jdk 提供的数据库访问 api, 显然就需要实现一套平台无关的通用代码, 支持各种不同数据库的访问(oracle, mysql, PostgreSQL, Microsoft Sql Server…)
这种需求就通过桥接模式进行了有效解决。
首先看一下 jdbc 的基本使用流程。
- 根据数据库类型, 选择对应的驱动类进行加载:
//加载MySql驱动
Class.forName("com.mysql.jdbc.Driver")
//加载Oracle驱动
//Class.forName("oracle.jdbc.driver.OracleDriver")
- 获得数据库连接:
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase", "root", "root");
- 创建Statement 或 PreparedStatement对象:
//conn.createStatement();
conn.prepareStatement(sql);
现在可以分析一下, JDBC 是如何应用桥接模式的。
再次回顾一下桥接模式的泛化结构图, 以此为基础分析 JDBC 是如何使用该模式的
- 桥接模式泛化结构图
值得注意的是,java.sql.*
中的众多接口和类里,java.sql.DriverManager
接口是桥接模式应用的出发点, 这个DriverManager
接口就对应于上图中Abstraction
, 而imp
则对应于registeredDrivers
public class DriverManager {
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
= new CopyOnWriteArrayList<>();
// 省略
进而可以看出, 上图中的 Implementor
好像对应于 DriverInfo
, 于是查看 DriverInfo
源码, 发现DriverInfo
并不是一个接口或者抽象类, 它只是简单地封装了有2个的成员变量 Driver, DriverAction
,
class DriverInfo {
final Driver driver;
DriverAction da;
DriverInfo(Driver driver, DriverAction action) {
this.driver = driver;
da = action;
}
@Override
public boolean equals(Object other) {
return (other instanceof DriverInfo)
&& this.driver == ((DriverInfo) other).driver;
}
@Override
public int hashCode() {
return driver.hashCode();
}
@Override
public String toString() {
return ("driver[className=" + driver + "]");
}
DriverAction action() {
return da;
}
}
查看一下发现 Driver, DriverAction
的定义是 Interface
, 由于 DriverAction 的引用在 DriverManager 中并未调用, 所以可以判断 Driver
对应于桥接模式中的 Implementor
, 。
可以看出,不同的数据库厂商提供的驱动包, 首先必须实现 Driver, DriverAction
定义的接口, 另外还需要实现 Driver, DriverAction
中所进一步引用的接口。 以 Driver
中的一个关键方法 connect(url, info)
为例, 由于该方法的返回值被定义为 java.sql.Connection
, 数据库驱动包中,则必然需要体用 Connection
对应的实现
public interface Driver {
// ... 代码略 ...
Connection connect(String url, java.util.Properties info)
throws SQLException;
// ... 代码略 ...
}
现在理清了如下线索:
- 桥接模式中的 Abstraction -->
DriverManager
- 桥接模式中的 Implementor -->
Driver
还需解决的问题:
- 桥接模式中的 Operation --> ?
- 桥接模式中的 RefinedAbstrction --> ?
Operation 对应的定义比较容易发现, 因为桥接模式中的 Operation 必然会调用 Implementor
, 此时再次查看 DriverManager
的源码, 查找使用了 registeredDrivers
的地方, 发现如下方法
private static Connection getConnection(
// ... 省略部分代码 ...
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// ... 省略部分代码 ...
}
getConnection
方法中,在已经注册的 registeredDrivers
中,遍历调用 connect
方法, 尝试建立数据库连接,建立成功则予以返回。
这样就明确了:
- 桥接模式中的 Operation -->
DriverManager @ getConnection
剩下的问题就是 RefinedAbstraction 对应于什么?
可能会有人认为 RefinedAbstract 对应于 java.sql.Connection
接口中, 会用到的 Statement
等同在 java.sql.*
中的接口
但是观察桥接模式的泛化结构图就会发现, 桥接模式所定义的 RefinedAbstraction
中所定义的接口, 与 Abstraction
所对应的接口, 应该是继承关系。 且 RefinedAbstraction 中定义的方法中,会对 Abstraction 接口中的方法进行调用。
从 DriverManager 出发进行查找, 发现 jdk 中并没有类继承 DriverManager。
思考: 这是否意味着桥接 JDBC 并未完整的应用桥接模式。
答案: 否
前面提到过, 由于 getConnection() 方法的返回类型为 java.sql.Connection
, 且该方法调用了 driver.connect(url,info)
来获取 Connection
, 由于 Driver
类的角色是桥接模式中的 Implementor, 这就保确保了 Connection
接口的实现以及 Connection 接口中, 进一步定义和引用的接口, 都会由数据库驱动商提供,这就确保了 java.sql.*
中的各类接口都会由对应的数据库驱动提供实现。
进一步的, 对于使用 JDBC 的程序员, 可以继承 java.sql.*
的接口, 自定义一些数据库访问接口,而这种变化, 与 Driver 的变化就相互独立, 各不影响了。
总结桥接模式在 JDBC 中使用的对应关系:
- 桥接模式中的 Abstraction -->
DriverManager
, 以及传递引用到的 java.sql.* 中的Connection
等其他接口- 桥接模式中的 Implementor -->
Driver
- 桥接模式中的 Operation -->
DriverMaanager#getConnection()
- 桥接模式中的 RefinedAbstrction --> 程序员自行继承
java.sql.*
中接口而定义扩展的新接口
总结
理解桥接模式的关键在于理解 “桥” 是什么, “桥”两侧连接的又是什么。