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