桥接模式

桥接模式

案例

公司欲开发一个数据转换工具,可以将数据库中的数据转换成多种文件格式,例如txt、xml、pdf等格式,同时该工具需要支持多种不同的数据库。下面我们先通过继承的方式来完成这一目的:

1.首先定义了一个抽象类:

/**
 * 文件父类
 */
public abstract class File {
    public abstract void translate();
}

2.两个转化为不同格式的子类:

txt 格式

/**
 * 转化为 txt 格式的文件类
 */
public class TxtFile extends File {
    public void translate() {
        System.out.println("转化为 txt 文件格式");
    }
}

xml 格式

/**
 * 转化为 xml 格式的文件类
 */
public class XmlFile extends File {
    public void translate() {
        System.out.println("转化为 xml 文件格式");
    }
}

3.再通过继承的方式从数据库读取数据:

从 mysql 读取数据并转化为 txt 格式

/**
 * 从 mysql 读取数据后转化为 txt 格式的文件类
 */
public class MysqlToTxt extends TxtFile {
    @Override
    public void translate() {
        System.out.println("从 mysql 数据库读取数据");
        super.translate();
    }
}

从 mysql 读取数据并转化为 xml 格式

/**
 * 从 mysql 读取数据后转化为 xml 格式的文件类
 */
public class MysqlToXml extends XmlFile {
    @Override
    public void translate() {
        System.out.println("从 mysql 数据库读取数据");
        super.translate();
    }
}

从 oracle 读取数据并转化为 txt 格式

/**
 * 从 oracle 读取数据后转化为 txt 格式的文件类
 */
public class OracleToTxt extends TxtFile {
    @Override
    public void translate() {
        System.out.println("从 oracle 数据库读取数据");
        super.translate();
    }
}

从 oracle 读取数据并转化为 xml 格式

/**
 * 从 oracle 读取数据后转化为 xml 格式的文件类
 */
public class OracleToXml extends XmlFile {
    @Override
    public void translate() {
        System.out.println("从 oracle 数据库读取数据");
        super.translate();
    }
}

4.测试:

/**
 * 测试类
 */
public class Main {
    public static void main(String[] args) {
        File file = new MysqlToTxt();
        file.translate();
        System.out.println("------------------------");
        file = new MysqlToXml();
        file.translate();
        System.out.println("------------------------");
        file = new OracleToTxt();
        file.translate();
        System.out.println("------------------------");
        file = new OracleToXml();
        file.translate();
    }
}

5.测试结果:

从 mysql 数据库读取数据
转化为 txt 文件格式
------------------------
从 mysql 数据库读取数据
转化为 xml 文件格式
------------------------
从 oracle 数据库读取数据
转化为 txt 文件格式
------------------------
从 oracle 数据库读取数据
转化为 xml 文件格式

虽然我们通过继承的方式完成了转化的过程,但是如果我们再增加一种转化为 Excel 格式的文件的话。我们需要增加三个类,例如ExcelFileMysqlToExcelOracleToExcel三个类。而如果我们要增加另一种数据读取方式的话,比如从 SQL server 读取数据,我们可能会增加SqlserverToTxtSqlserverToXMl两个类。这样其实我们的系统在扩展时还是较为麻烦的。下面介绍桥接模式对这一系统进行改进。

模式介绍

桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interfce)模式。

说明:桥接模式是一种很实用的结构型设计模式,如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。

角色构成:

  • Abstraction(抽象类):用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个Implementor(实现类接口)类型的对象并可以维护该对象,它与Implementor之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。
  • RefinedAbstraction(扩充抽象类):扩充由Abstraction定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在Abstraction中声明的抽象业务方法,在RefinedAbstraction中可以调用在Implementor中定义的业务方法。
  • Implementor(实现类接口):定义实现类的接口,这个接口不一定要与Abstraction的接口完全一致,事实上这两个接口可以完全不同,一般而言,Implementor接口仅提供基本操作,而Abstraction定义的接口可能会做更多更复杂的操作。Implementor接口对这些基本操作进行了声明,而具体实现交给其子类。通过关联关系,在Abstraction中不仅拥有自己的方法,还可以调用到Implementor中定义的方法,使用关联关系来替代继承关系。
  • ConcreteImplementor(具体实现类):具体实现Implementor接口,在不同的ConcreteImplementor中提供基本操作的不同实现,在程序运行时,ConcreteImplementor对象将替换其父类对象,提供给抽象类具体的业务操作方法。

UML 类图:

bridge

在使用桥接模式时,我们首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。

代码改造

在我们这一案例中,文件转化的格式以及读取数据的来源是代码中具有的两个个独立变化的维度,我们就为其设计独立的继承等级结构,来达到解耦的目的。

1.首先是文件抽象类及其子类:

抽象文件类:

/**
 * 抽象类角色
 */
public abstract class File {
    protected Database dataBase;

    public void setDataBase(Database dataBase) {
        this.dataBase = dataBase;
    }

    public abstract void translate();
}

txt 文件格式类:

/**
 * 扩充抽象类角色:txt 文件格式类
 */
public class TxtFile extends File {
    public void translate() {
        dataBase.load();
        System.out.println("转化为 txt 文件格式");
    }
}

xml 文件格式类:

/**
 * 扩充抽象类角色:xml 文件格式类
 */
public class XmlFile extends File {
    public void translate() {
        dataBase.load();
        System.out.println("转化为 xml 文件格式");
    }
}

2.定义实现类接口及其实现:

/**
 * 实现类接口角色
 */
public interface Database {
    void load();
}

mysql 读取数据接口实现:

/**
 * 具体实现类角色:从 mysql 中读取数据
 */
public class MysqlDatabase implements Database {
    public void load() {
        System.out.println("从 mysql 数据库读取数据");
    }
}

oracle 读取数据接口实现:

/**
 * 具体实现类角色:从 oracle 中读取数据
 */
public class OracleDatabase implements Database {
    public void load() {
        System.out.println("从 oracle 数据库读取数据");
    }
}

3.测试类:

public class Main {
    public static void main(String[] args) {
        File file = new TxtFile();
        file.setDataBase(new MysqlDatabase());
        file.translate();
        System.out.println("------------------------");
        file.setDataBase(new OracleDatabase());
        file.translate();
        System.out.println("------------------------");
        file = new XmlFile();
        file.setDataBase(new MysqlDatabase());
        file.translate();
        System.out.println("------------------------");
        file.setDataBase(new OracleDatabase());
        file.translate();
    }
}

4.测试结果:

从 mysql 数据库读取数据
转化为 txt 文件格式
------------------------
从 oracle 数据库读取数据
转化为 txt 文件格式
------------------------
从 mysql 数据库读取数据
转化为 xml 文件格式
------------------------
从 oracle 数据库读取数据
转化为 xml 文件格式

可以看到,使用桥接模式也达到了同样的效果。而且在扩展的时候,如果要增加一种问价格式比如 Excel 格式,只需新增一个ExcelFile类。而扩展数据源读取方式如 SQL server 时,只需新增 SqlserverDabase类。最后通过在客户端配合使用就可以达到扩展的目的。

模式应用

与数据库交互在我们的开发中基本上可以说是必不可少的,而我们之所以能够与不同的数据库进行交互,就是因为 JDBC 的存在,它为我们与数据库交互制定了一套规范,使得我们只需要引入相关数据库驱动,就可以方便的与不同的数据库交互了。下面首先看一下与数据库连接的代码。

1.pom 依赖:这里引入了 mysql 和 oracle 的依赖,便于分析源码

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.0.2</version>
    </dependency>
    <dependency>
        <groupId>cn.easyproject</groupId>
        <artifactId>ojdbc6</artifactId>
        <version>12.1.0.2.0</version>
    </dependency>
</dependencies>

2.连接数据库:

public class Main {
    // 数据库驱动全类名
    private static final String driverClass = "com.mysql.jdbc.Driver";

    // 数据库地址
    private static final String url = "jdbc:mysql://localhost:3306/test";

    // 用户名
    private static final String user = "user";

    // 密码
    private static final String password = "password";

    private static Connection connection;

    public static void main(String[] args) {
        try {
            // 加载驱动
            Class.forName(driverClass);
            // 获取连接
            connection = DriverManager.getConnection(url, user, password);
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

可以看到,我们首先引入相关依赖后,再配置数据库连接信息,然后加载驱动,最后就可以通过java.sql.DriverManager类来获取连接了。那么他是如何获取连接的呢,下面去它的源码内部getConnection()方法分析一下:

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    
    // 循环所有注册的 Driver,这里的 registeredDrivers 集合里面就包含了我们在客户端中加载的驱动
    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());
                // 通过不同的 Driver 得到不同 Connection 实现
                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());
        }

    }

}

这里获取Connection连接是根据不同的Driver来获取的。如果是 mysql,则是com.mysql.jdbc.Driver返回com.mysql.jdbc.Connection类,如果是oracle.jdbc.OracleDriver则返回Oracle中的Connection实现。但是我们在调用过程中依旧是用其接口定义,这里充分运用了面向对象多态的性质。

下面是其类之间的UML类图:

bridge-jdbc

从图中我们可以看到Connection接口和Driver接口是桥接模式中两个独立变化的事物,而在 mysql 和 oracle 中有不同的实现。从设计上来说,这一模式体现了很多面向对象设计原则的思想,包括“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”等。熟悉桥接模式有助于我们深入理解这些设计原则,也有助于我们形成正确的设计思想和培养良好的设计风格。

总结

1.主要优点

  • 分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维度组合对象。
  • 在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。
  • 桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。

2.主要缺点

  • 桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。
  • 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。

3.适用场景

  • 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
  • “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
  • 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
  • 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

参考资料

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/bridge
转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/13950501.html

原文地址:https://www.cnblogs.com/phoegel/p/13950501.html