桥接模式( Bridge Pattern ): 可以变化的抽象类与接口

  1. 参考书籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》
  2. Bridge pattern - Wiki

前言

桥接模式是一种提及频率很高, 应用频率较少的设计模式。 桥接模式之所以被频繁提及, 是因为它的设计意图提到了“解耦”, 然而它的解耦方式却常常被很多人误解。

设计模式用前须知

  • 设计模式种一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
  • 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句话往往只对框架/ 工具包的设计才有真正的意义框架和工具包存在的意义,就是为了让其他的程序员使用,在其基础上实现功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
  • 对于应用程序的编写者, 从理论上来说, 所有应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。

桥接模式 ( Bridge )

  • 设计意图

    • GOF: “ 解耦抽象与实现, 使得二者可以独立地变化。”
      • 80% 的程序员听到这句话, 第一反应都是: 一个接口(抽象类)可以对应多个实现类, 通过接口可以解耦抽象与实现。
      • 有趣的是, 第一反应是错误的。 原因是设计意图的后半句话没有被满足: “二者可以独立地变化
        • 当一个接口被定义好以后,实现类的确可以随意变化, 但是接口可以独立于实现类随意变化吗? 如果你向接口中添加一个方法, 那之前所有实现了该接口的类是否还满足该接口?
        • 答案显然是 “不”。
    • GOF举例 :
      • 考虑一个用于编写图形界面的工具包/类库, 假设该工具包支持编写可移植的跨平台界面, 其中定义了一个窗口的抽象(接口/抽象类)Window。 为了支持X平台和PM平台, 需要两个实现类 XWindow, PMWindow.
      • 到目前为止, 一切都很好。 可是当你试图想要扩展Window的抽象时, 就会发现问题。 假设我们要定义一个抽象类 IconWindow, 专用于描述 “图标窗口” 所需要实现的方法。 此时由于IconWindow也需要支持X平台和PM平台, 所以就需要再实现XIConWindow, PMIconWindow。
      • 这里写图片描述
      • 从上述结构可以看出, 每增加一种抽象类abcWindow, 都需要额外实现两个平台的实现类 XabcWindow, PMabcWindow 。 这个问题的深层次原因是 : 接口和其实现类是存在耦合关系的, 每当你想要改变接口定义的时候,实现类必须也有相应的变化。
  • 解决方案
    这里写图片描述

图例说明


在这里插入图片描述

这种关系在 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.* 中接口而定义扩展的新接口

总结

理解桥接模式的关键在于理解 “桥” 是什么, “桥”两侧连接的又是什么。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值