设计模式|桥接模式

【设计模式|桥接模式】桥接模式是一种结构型设计模式,英文是 Bridge Design Pattern。在 GoF 的《设计模式》一书中,桥接模式是这么定义的:Decouple an abstraction from its implementation so that the two can vary independently。翻译成中文就是:将抽象和实现解耦,让它们可以独立变化。单从官方概念来理解,相当晦涩,通常对抽象与实现的第一反应就是接口(或抽象类)与实现类的继承关系,接口和实现独立变化就更加让人难以理解。
关于桥接模式,还有另外一种理解方式:一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。再配合类图(https://zh.wikipedia.org/wiki/%E6%A9%8B%E6%8E%A5%E6%A8%A1%E5%BC%8F)就比较容易理解一些。

设计模式|桥接模式
文章图片

桥接模式所涉及的角色有:

  • 抽象(Abstraction)角色:抽象角色,采用抽象类方式,并持有一个实现角色对象的引用。
  • 具体抽象(RefinedAbstraction)角色:具体抽象角色具体实现。
  • 实现(Implementor)角色:给出实现角色的接口。实现化角色应当只给出抽象层操作,抽象角色持有抽象角色并只给出基于抽象角色的操作。
  • 具体实现(ConcreteImplementor)角色:这个角色给出实现角色接口的具体实现。
可以看到抽象和实现是两个角色,是调用者和被调用者的关系,通过组合关系使抽象角色持有实现角色,通过组合关系来替代继承关系,避免继承层次过多。
接下来通过代码理解一下桥接模式,当我们办理银行卡时,有不同的银行(工商银行,建设银行)可选择,每个银行有不同的卡(储蓄卡,信用卡),所以银行和卡就相当于两个独立变化的维度,我们将银行和卡进行抽象,将银行作为抽象角色,将卡作为实现角色,通过组合的方式使银行持有卡的接口引用,将其组合在一起。
public class BridgePattern {/** * 银行抽象类,桥接模式的抽象部分 */ abstract static class Bank { protected Card card; public Bank(Card card) { this.card = card; }abstract Card applyCard(); }/** * 建设银行实现类 */ static class CCBBank extends Bank {public CCBBank(Card card) { super(card); }@Override Card applyCard() { System.out.print("申请建设银行 "); card.applyCard(); return card; } }/** * 工商银行实现类 */ static class ICBCBank extends Bank {public ICBCBank(Card card) { super(card); }@Override Card applyCard() { System.out.print("申请工商银行 "); card.applyCard(); return card; } }/** * 银行账号,桥接模式的实现部分接口 */ interface Card { /** * 申请卡 * * @return */ Card applyCard(); /** * 查看卡类型 */ void showCardType(); }/** * 信用卡 */ static class CreditCard implements Card { @Override public CreditCard applyCard() { return new CreditCard(); }@Override public void showCardType() { System.out.println("信用卡"); } }/** * 储蓄卡 */ static class DebitCard implements Card { @Override public DebitCard applyCard() { return new DebitCard(); }@Override public void showCardType() { System.out.println("储蓄卡"); } }public static void main(String[] args) { Bank icbcBank = new ICBCBank(new CreditCard()); Card icbcCard = icbcBank.applyCard(); icbcCard.showCardType(); Bank ccbBank = new CCBBank(new DebitCard()); Card ccbCard = ccbBank.applyCard(); ccbCard.showCardType(); }}

输出结果
申请工商银行 信用卡 申请建设银行 储蓄卡

通过使用桥接模式,就使得银行和卡这两个维度可以独立变化,互不干扰。
JDBC数据库访问接口API正是经典的桥接模式实现,具体的代码如下所示:
Class.forName("com.mysql.jdbc.Driver"); // 加载及注册JDBC驱动程序 String url = "jdbc:mysql://localhost:3306/mydb?user=root&password=****"; Connection con = DriverManager.getConnection(url);

如果我们想要把 MySQL 数据库换成 Oracle 数据库,只需要把第一行代码中的 com.mysql.jdbc.Driver 换成 oracle.jdbc.driver.OracleDriver 便可。
com.mysql.jdbc.Driver类代码实现如下:
package com.mysql.jdbc; import java.sql.DriverManager; import java.sql.SQLException; public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { }static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } } }

这里涉及另外一个知识,Class.forName和ClassLoader加载类的区别:ClassLoader只能将类加载到JVM方法区(JDK8为元数据区),但是Class.forName除了加载类外,还可以直接加载类中的静态代码块。
所以当执行 Class.forName(“com.mysql.jdbc.Driver”) 这条语句的时候,会将 com.mysql.jdbc.Driver 注册到 DriverManager 类中。
java.sql.DriverManager类代码精简后实现如下:
package java.sql; public class DriverManager {private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>(); private DriverManager() { }public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {registerDriver(driver, null); }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); }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())); }private static Connection getConnection(String url, java.util.Properties info, Class caller) throws SQLException { for (DriverInfo aDriver : registeredDrivers) { 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); } } }}

可以看到注册的过程无非是把Driver对象存放进入了CopyOnWriteArrayList类型的registeredDrivers变量中,获取连接的时候,从registeredDrivers中取出对应的Driver并调用相应的connect方法进行连接。
JDBC 这个例子中,JDBC DriverManager就相当于“抽象”角色,java.sql.Driver相当于实现角色,具体的 Driver(比如com.mysql.jdbc.Driver)就相当于实现角色的具体实现。JDBCDriverManager 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。
JDBC的类族是由SUN公司设计了一套接口,再由各个数据库公司实现接口,我们在调用的过程中只需要使用接口去定义,然后在加载Driver的过程中底层代码会给我们选择好接口真正的实现类,以此来实现真正的数据库连接。
总体上来讲,桥接模式的原理比较难理解,但代码实现相对简单。在 GoF 的《设计模式》一书中,桥接模式被定义为:将抽象和实现解耦,让它们可以独立变化。还有另外一种更加简单的理解方式:一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。对于第一种 GoF 的理解方式,弄懂定义中“抽象”和“实现”两个概念,是理解它的关键。定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的角色,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”,也并非“接口的实现类”,而是一套独立的角色。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起。
桥接模式的优点使抽象和实现可以沿着各自的维度来变化,所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,使它们各自都具有自己的子类,从而获得多维度组合对象。桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。
桥接模式的缺点是原理比较难理解,会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。还要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也并非一件易事。

    推荐阅读