1. 什么是桥接模式
桥接模式。桥接模式的代码实现非常简单,但是理解起来稍微有点难度,并且应用场景也比较局限,所以,相当于代理模式来说,桥接模式在实际的项目中并没有那么常用
桥接模式,也叫作桥梁模式,英文是 Bridge Design Pattern。这个模式可以说是 23 种设计模式中最难理解的模式之一了。我查阅了比较多的书籍和资料之后发现,对于这个模式有两种不同的理解方式。
当然,这其中“最纯正”的理解方式,当属 GoF 的《设计模式》一书中对桥接模式的定义。毕竟,这 23 种经典的设计模式,最初就是由这本书总结出来的。在 GoF 的《设计模式》一书中,桥接模式是这么定义的:“Decouple an abstraction from its implementation so that the two can vary independently。”翻译成中文就是:“将抽象和实现解耦,让它们可以独立变化”
关于桥接模式,很多书籍、资料中,还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。 ”通过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于,“组合优于继承”设计原则。我们重点看下 GoF 的理解方式。
GoF 给出的定义非常的简短,单凭这一句话,估计没几个人能看懂是什么意思。所以,我们通过 JDBC 驱动的例子来解释一下。JDBC 驱动是桥接模式的经典应用。我们先来看一下,如何利用 JDBC 驱动来查询数据库。具体的代码如下所示:
//加载驱动
Class.forName("com.mysql.jdbc.Driver");//加载及注册JDBC驱动程序
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
// 连接,获得数据库对象connection
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
rs.getString(1);
rs.getInt(2);
}
如果我们想要把 MySQL 数据库换成 Oracle 数据库,只需要把第一行代码中的 com.mysql.jdbc.Driver 换成 oracle.jdbc.driver.OracleDriver 就可以了。当然,也有更灵活的实现方式,我们可以把需要加载的 Driver 类写到配置文件中,当程序启动的时候,自动从配置文件中加载,这样在切换数据库的时候,我们都不需要修改代码,只需要修改配置文件就可以了。
不管是改代码还是改配置,在项目中,从一个数据库切换到另一种数据库,都只需要改动很少的代码,或者完全不需要改动代码,那如此优雅的数据库切换是如何实现的呢?
要弄清楚这个问题,我们需要从 com.mysql.jdbc.Driver 这个类的代码看起
package com.mysql.cj.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
结合 com.mysql.jdbc.Driver 的代码实现,我们可以发现,当执行 Class.forName(“com.mysql.jdbc.Driver”) 这条语句的时候,实际上是做了两件事情。
- 第一件事情是要求 JVM 查找并加载指定的 Driver 类
- 第二件事情是
执行该类的静态代码,也就是将 MySQL Driver 注册到 DriverManager 类中。
现在,我们再来看一下,DriverManager 类是干什么用的。具体的代码如下所示。当我们把具体的 Driver 实现类(比如,com.mysql.jdbc.Driver)注册到 DriverManager 之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver 实现类来执行。而 Driver 实现类都实现了相同的接口(java.sql.Driver ),这也是可以灵活切换 Driver 的原因。
public class DriverManager {
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();
//...
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
//...
public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
if (driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver));
} else {
throw new NullPointerException();
}
}
// 连接,获得数据库对象connection
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()));
}
//...
}
桥接模式的定义是“将抽象和实现解耦,让它们可以独立变化” 。那弄懂定义中“抽象”和“实现”两个概念,就是理解桥接模式的关键。那在 JDBC 这个例子中,什么是“抽象”?什么是“实现”呢?
1.1 什么是JDBC
JDBC(Java Database Connectivity, Java 数据库连接)是 一 种可用于执行 SQL 语句的 Java API(Application Programming Interface, 应用程序设计接口)。它由一些 Java 语言编写的类和接口组成。JDBC 为数据库应用开发人员、数据库前台开发人员提供了一种标准的应用程序设计接口, 使开发人员可以用纯 Java 语言编写完整的数据库应用程序。JDBC 通过调用其接口提供的方法, 提供了 Java 应用程序与各种数据库服务器之间的连接服务, 它支持 ANSI SQL- 92 标准, 实现了从 Java 程序内调用标准的 SQL 命令对数据库进行查询、插入、删除和更新等操作, 并确保数据事务的正常进行。JDBC API 主要用来连接数据库并通过 SQL 语句操作数据库。它可以执行一般的 SQL 语句、动态 SQL 语句及带 IN 和 OUT参数的存储过程。通过使用 JDBC 简化了开发数据库应用的复杂度, 加快了开发速度。开发人员可以很方便地将 SQL 语句传送给几乎任何一种数据库。而且 JDBC 支持所有的操作系统, 使得 Java 应用程序不再需要为每个应用程序编写额外的驱动。
总结一下就是JDBC是用于Java编程语言和数据库之间的数据库无关连接的标准Java API,换句话说:JDBC是用于在Java语言编程中与数据库连接的API。 ,它是一种用于数据库访问的应用程序 API ,由一组用Java语言编写的类和接口组成,有了JDBC就可以用统一的语法对多种关系数据库进行访问,而不用担心其数据库操作语言的差异 。 有了JDBC,就不必为访问Mysql数据库专门写一个程序,为访问Oracle又专门写一个程序等等。
JDBC库包括通常与数据库使用相关,如下面提到的每个任务的API
- 连接到数据库
- 创建SQL或MySQL语句
- 在数据库中执行SQL或MySQL查询
- 查看和修改结果记录
//加载驱动
Class.forName("com.mysql.jdbc.Driver");//加载及注册JDBC驱动程序
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
// 连接,获得数据库对象connection
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
rs.getString(1);
rs.getInt(2);
}
1.1.1 JDBC架构
- JDBC API:提供应用程序到JDBC管理器连接。
- JDBC驱动程序API:支持JDBC管理器到驱动程序连接。
JDBC API使用驱动程序管理器并指定数据库的驱动程序来提供与异构数据库的透明连接。
JDBC 是实现 Java 应用程序与各种不同数据库对话的一种机制。JDBC 由两部分与数据库独立的 API 组成, 一部分是面向程序开发人员的 JDBC API, 另一部分是面向底层的 JDBC Driver API。JDBC 提供了一个通用的 JDBC Driver Manger, 用来管理各种数据库软件商提供的 JDBC 驱动程序, 从而访问其数据库。
使用JDBC编程,可让开发人员从复杂的驱动器调用命令和函数中解脱出来,可以致力于应用程序中的关键地方。JDBC支持不同的关系数据库,这使得程序的可移植性大大加强。JDBC API是面向对象的,可以让用户把常用的方法封装为一个类,以备后用。但是它也有缺点,一是使用JDBC,访问数据记录的速度会受到一定程度的影响。二是JDBC结构中包含不同厂家的产品,这就给更改数据源带来了很大的麻烦。
JDBC 是实现 Java 应用程序与各种不同数据库对话的一种机制。JDBC 由两部分与数据库独立的 API 组成, 一部分是面向程序开发人员的 JDBC API, 另一部分是面向底层的 JDBC Driver API。JDBC 提供了一个通用的 JDBC Driver Manger, 用来管理各种数据库软件商提供的 JDBC 驱动程序, 从而访问其数据库。
JDBC 的基本层次结构由 Java 程序、JDBC 驱动程序管理器、驱动程序和数据库四部分组成。
1.1.2 JDBC核心接口与类
JDBC核心类库包含在java.sql包中。
接口:
Connection:此接口具有用于联系数据库的所有方法。 连接(Connection)对象表示通信上下文,即,与数据库的所有通信仅通过连接对象。在连接上下文中执行SQL语句并返回结果。- Statement:使用此接口创建的对象将SQL语句提交到数据库。 除了执行存储过程之外,一些派生接口还接受参数。
- ResultSet :在使用
Statement对象执行SQL查询后,这些对象保存从数据库检索的数据。 它作为一个迭代器并可移动ResultSet对象查询的数据。
类:
DriverManager:此类管理数据库驱动程序列表。使用JDBC驱动程序之前,必须先将驱动程序加载并注册后才可以使用,同时提供方法来建立与数据库的连接。(加载到内存中才能使用)。 使用通信子协议将来自java应用程序的连接请求与适当的数据库驱动程序进行匹配。在JDBC下识别某个子协议的第一个驱动程序将用于建立数据库连接。Driver:此接口处理与数据库服务器的通信。我们很少会直接与Driver对象进行交互。 但会使用DriverManager对象来管理这种类型的对象。 它还提取与使用Driver对象相关的信息。SQLException:此类处理数据库应用程序中发生的任何错误。
1.1.3 Driver的实现
PostgrepSql Driver
package org.postgresql;
public static void register() throws SQLException {
if (isRegistered()) {
throw new IllegalStateException("Driver is already registered. It can only be registered once.");
} else {
Driver registeredDriver = new Driver();
DriverManager.registerDriver(registeredDriver);
Driver.registeredDriver = registeredDriver;
}
}
Mysql Driver
package com.mysql.cj.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
1.2 什么是“抽象” & 什么是“实现”
JDBC 本身就相当于“抽象”。注意,这里所说的“抽象”,指的并非“抽象类”或“接口”,而是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。注意,这里所说的“实现”,也并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”。JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。
2. 桥接模式的应用
现在对不同手机类型的不同品牌实现操作编程(比如:开机,关机,上网,打电话)
2.1 传统方法对应的类图
传统方法解决手机操作问题分析
- 扩展性问题(类爆炸),如果我们再增加手机的样式(旋转式),就需要增加各个手机品牌手机的类,同样如果我们增加一个手机品牌,也要在各个手机样式类下增加
- 违反了单一职责原则,当我们增加手机样式时,要同时增加所有品牌的手机,这样增加了代码维护成本。
2.2 桥接模式
- 桥接模式(Brige ) 是指:将实现与抽象放在两个不同的类层次种,使两个层次可以独立改变
- 是一种结构型设计模式
- Bridge模式基于类的最小设计模式,通过使用封装 聚合 继承等行为让不同的类承担不同的职责。它的主要特点时把抽象(abstraction)与行为实现(Implementation)分离出来,从而可以保持各部分的独立性以及应对他们的功能拓展
2.3 代码实现
2.3.1 手机品牌
package com.evan.brige;
// 手机品牌
public interface Brand {
void open();
void close();
void call();
}
package com.evan.brige;
public class Vivo implements Brand {
@Override
public void open() {
System.out.println("Vivo手机开机");
}
@Override
public void close() {
System.out.println("Vivo手机关机");
}
@Override
public void call() {
System.out.println("Vivo手机打电话");
}
}
package com.evan.brige;
public class XiaoMi implements Brand {
@Override
public void open() {
System.out.println("小米手机开机");
}
@Override
public void close() {
System.out.println("小米手机关机");
}
@Override
public void call() {
System.out.println("小米手机打电话");
}
}
2.3.2 模板类
package com.evan.brige;
public abstract class Phone {
private Brand brand;
public Phone(Brand brand){
this.brand = brand;
}
public void open() {
brand.open();
}
public void close() {
brand.close();
}
public void call() {
brand.call();
}
}
2.3.3 手机形态类型
直板形态手机
package com.evan.brige;
public class UpRightPhone extends Phone {
public UpRightPhone(Brand brand) {
super(brand);
}
public void open() {
System.out.println("直板手机");
super.open();
}
public void close() {
System.out.println("直板手机");
super.close();
}
public void call() {
System.out.println("直板手机");
super.call();
}
}
package com.evan.brige;
public class Client {
public static void main(String[] args) {
UpRightPhone phone1 = new UpRightPhone(new XiaoMi());
phone1.close();
phone1.open();
phone1.close();
System.out.println("============================");
UpRightPhone phone2 = new UpRightPhone(new Vivo());
phone2.close();
phone2.open();
phone2.close();
}
}
折叠形态手机
若各大厂商出折叠手机
package com.evan.brige;
public class FolderPhone extends Phone {
public FolderPhone(Brand brand) {
super(brand);
}
public void open() {
System.out.println("Folder手机");
super.open();
}
public void close() {
System.out.println("Folder手机");
super.close();
}
public void call() {
System.out.println("Folder手机");
super.call();
}
}
package com.evan.brige;
public class Client {
public static void main(String[] args) {
UpRightPhone phone1 = new UpRightPhone(new XiaoMi());
phone1.close();
phone1.open();
phone1.close();
System.out.println("============================");
UpRightPhone phone2 = new UpRightPhone(new Vivo());
phone2.close();
phone2.open();
phone2.close();
System.out.println("============================");
FolderPhone phone3 = new FolderPhone(new XiaoMi());
phone3.close();
phone3.open();
phone3.close();
System.out.println("============================");
FolderPhone phone4 = new FolderPhone(new Vivo());
phone4.close();
phone4.open();
phone4.close();
}
}
3. 桥接模式总结
3.1 桥接模式的细节
- 实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来,这有助于系统进行分层设计,从而产生更好的结构化系统
- 对于系统的高层 部分,只需要知道抽象部分和实现部分的接口就可以了,其他的部分由具体业务来完成
- 桥接模式,可以减少子类的个数,降低了系统的管理和维护成本
- 桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程
- 桥接模式要求正确识别出系统中两个独立变化维度(抽象和实现),因此其使用范围有一定的局限性,即需要有这样的场景
3.2 桥接模式的细节
-
主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
-
何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。
-
如何解决:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。
-
应用实例:猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,灵魂相当于抽象化,肉体相当于实现。猪八戒有两个实现一个是猪,另一个是天蓬元帅。
-
注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。
3.3 桥接模式的优缺点
优点:
-
抽象和实现的分离。
-
优秀的扩展能力。
-
实现细节对客户透明。
缺点:
桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
3.4 使用场景:
-
如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
-
对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
-
一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。