本文參考
網上對於JDBC與橋接模式的理解各有不同,在這片文章里提出的是我個人對於二者的理解,本文參考的其它博文如下:
https://blog.csdn.net/paincupid/article/details/43614029
http://c.biancheng.net/view/1320.html
https://www.jianshu.com/p/775cb53a4da2
楊曙.基於設計模式之橋接模式的淺析[J].電腦知識與技術,2013,9(02):433-434+443.
橋接模式的定義與特點
-
定義:
將抽象與實現分離,使它們可以獨立變化。它是用組合/聚合關系代替繼承關系來實現,從而降低了抽象和實現這兩個可變維度的耦合度。例如針對一個圖形,我們可以設計顏色和形狀兩個變化維度
-
優點:
由於抽象與實現分離,所以擴展能力強;實現細節對客戶透明
-
缺點:
由於聚合關系建立在抽象層,要求開發者針對抽象化進行設計與編程,這增加了系統的理解與設計難度
橋接模式的基本結構
-
Abstraction — 抽象化角色:
定義抽象的接口,包含一個對實現化角色的引用
-
Refined Abstraciotn — 擴展抽象化角色:
抽象化角色的子類,實現父類中的業務方法,並通過組合/聚合關系調用實現化角色中的業務方法
-
Implementor — 實現化角色:
定義具體行為、具體特征的應用接口,供擴展抽象化角色使用
-
Concrete Implemetor — 具體實現化角色:
實現化角色的具體實現
-
基本的模式結構類圖如下:
橋接模式的應用場景
-
當一個類存在兩個獨立變化的維度,且這兩個維度都需要進行擴展時
-
當一個系統不希望使用繼承或因為多層次繼承導致系統類的個數急劇增加時
-
當一個系統需要在構件的抽象化角色和具體化角色之間增加更多的靈活性時
JDBC源碼剖析
在不使用Spring、Hibernate等第三方庫的情況下,直接通過原生JDBC API連接MySQL數據庫,則有如下示例代碼:
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://<host>:<port>/<database>");
短短兩行代碼難以看出橋接模式的結構,下面先對源碼進行一定的分析,理解各個類和接口之間的關系
-
Class.forName()方法
該方法將返回與給定字符串名的類或接口相關聯的java.lang.Class類對象,用於在程序運行時的某個時刻,由客戶端調用,動態加載該類或該接口到當前線程中
Returns the Class object associated with the class or interface with the given string name.
Given the fully qualified name for a class or interface this method attempts to locate, load, and link the class or interface.
若Class.forName()加載的是一個類,也會執行類中包含的static { } 靜態代碼段
-
com.mysql.cj.jdbc.Driver類
MySQL將具體的java.sql.Driver接口的實現放到了NonRegisteringDriver中,com.mysql.cj.jdbc.Driver類僅包含一段靜態代碼,具體類圖如下:
其中最關鍵的是靜態代碼段中的 DriverManager.registerDriver(new Driver()) ,它會在客戶端調用Class.forName()方法加載com.mysql.cj.jdbc.Driver類的同時被執行,Driver類自身的一個實例被注冊到DriverManager(即保存到DriverManager的靜態字段registeredDrivers內),注冊過程的源碼如下:
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);
}
registeredDrivers靜態字段的類型是實現了List接口的CopyOnWriteArrayList類,它能夠保存進一步封裝java.sql.Driver接口的DriverInfo類實例,DriverInfo類的聲明代碼如下:
class DriverInfo {
final Driver driver;
DriverAction da;
DriverInfo(Driver driver, DriverAction action) {
this.driver = driver;
da = action;
}
// ……
}
引申:
DriverInfo還包裝了DriverAction,DriverAction會在Driver被取消注冊時被調用,DriverAction的源碼注釋如下:
The JDBC driver's static initialization block must call DriverManager.registerDriver(Driver, DriverAction) in order to inform DriverManager which DriverAction implementation to call when the JDBC driver is de-registered.
MySQL的Driver在向DriverManager進行注冊時,DriverAction被設置為null
-
DriverManager類
由上面的分析可得,Class.forName()方法調用后,com.mysql.cj.jdbc.Driver類被加載,並執行static { } 靜態代碼段,將com.mysql.cj.jdbc.Driver類實例注冊到DriverManager中。然后,客戶端會調用DriverManager.getConnection()方法獲取一個Connection數據庫連接實例,該方法的部分源碼如下:
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
// ……
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());
}
}
// ……
}
DriverManager.getConnection()方法會遍歷registeredDrivers靜態字段,獲取字段內保存的每一個Driver來嘗試響應客戶端的數據庫連接請求,若所有Driver都連接數據庫失敗,則提示連接失敗信息
-
Connection接口
Connection代表和特定數據庫的連接會話,能夠執行SQL語句並在連接的上下文中返回執行結果。
A connection (session) with a specific database. SQL statements are executed and results are returned within the context of a connection.
因此,DriverManager.getConnection()方法返回的Connection數據庫連接實例根據不同的數據庫有不同的實現,MySQL的Connection接口實現關系如下:
源碼類圖
根據源碼的分析,繪制類圖如下:
對Driver和Connection進行抽象,繪制類圖如下:
模式體現
橋接模式通過組合/聚合關系代替繼承關系,實現抽象化和實現化部分的解耦。以上述JDBC在MySQL中的簡略類圖為例,抽象化部分有Driver接口和Connection接口,實現化部分有DriverManager。對於不同的數據庫,Driver接口和Connection接口都有自己獨特的實現類
但是,和Driver接口不同的是,Connection接口與DriverManager類的關系只是聯系較弱的依賴關系,並不符合橋接模式的定義和特點。因此,在考慮橋接模式的情況下,可以再次將類圖進行簡化:
最后,我們將其它數據庫的Driver接口實現也考慮在內,繪制類圖如下:
橋接模式中的實現化(Implementor)角色對應上圖的Driver接口,具體實現化(Concrete Implementor)角色對應MysqlDriver、OracleDriver和MariadbDriver,擴展抽象化 (Refined Abstraction)角色對應DriverManager,不具有抽象化(Abstraction)角色作為擴展抽象化角色的父類
橋接模式的主要應用場景是某個類存在兩個獨立變化的維度,且這兩個維度都需要進行擴展,而現在僅有Driver一個變化維度,DriverManager沒有抽象化父類,它本身也沒有任何子類,因此我認為,在JDBC中,是一種簡化的橋接模式 —— 觀點一。
倘若JDBC針對Connection接口的設計不是將它作為Driver和DriverManager的"依賴"來處理,而是也作為一個變化的維度加入到橋接模式,或許能夠更好地體現JDBC對橋接模式的實現,一種"假想"的橋接模式如下:
其它觀點二:JDBC采用的是策略模式而不是橋接模式
https://www.zhihu.com/question/67735508
問題源自知乎,但是沒有任何人做出解答,因為這確實和策略模式十分相似,如果把橋接模式的抽象部分簡化來看,不去設計Abstraction,也就是用Refined Abstraction代替Abstraction,那么就類似於策略模式的Context來使用接口的對象
但是,橋接模式和策略模式的目的是不一樣的,策略模式屬於對象行為模式(描述對象之間怎樣相互協作共同完成單個對象都無法單獨完成的任務,以及怎樣分配職責),它的目的是封裝一系列的算法,使得算法可以相互替代,並在程序運行的不同時刻選擇合適的算法。而橋接模式屬於對象結構模式(描述如何將對象按某種布局組成更大的結構),它的目的是將抽象與實現分離,使它們可以獨立變化
因此,從設計的目的來看,JDBC采用的並不是策略模式,在一段程序中數據庫驅動並不存在頻繁地相互替換
其它觀點三:變化的維度一個是平台,另一個是數據庫
https://www.unclewang.info/learn/java/771/?tdsourcetag=s_pctim_aiomsg
這是我認同的一個觀點,引用原文的話
變的是平台和數據庫,平台在jvm這個層面就解決了,因為所有操作系統java基本都會提供對應JDK,這也是"Once Write,Run AnyWhere"的原因。而數據庫則是依托公司的具體實現,各個公司都提供對應的Driver類,我用DriverManager類進行懶加載
考慮數據庫的實際應用場景,我們可能在不同的操作系統上使用不同的數據庫,但是JVM的平台無關性使得我們不再有操作系統層面上的變化。假設不存在JVM,那么不同的客戶端加載和運行數據庫驅動程序的代碼自然也各有不同,即DriverManager會因操作系統的變化而變化,不同的操作系統可以有不同的注冊Driver的方式
不過因為存在JVM,我們現在不再有"平台"這一變化維度了
其它觀點四:變化的維度一個是客戶端應用系統,另一個是數據庫
https://www.jianshu.com/p/775cb53a4da2
一個比較獨特的觀點,引用原文的話
應用系統作為一個等級結構,與JDBC驅動器這個等級結構是相對獨立的,它們之間沒有靜態的強關聯。應用系統通過委派與JDBC驅動器相互作用,這是一個橋梁模式的例子。
原文筆者不認為DriverManager作為Refined Abstraction角色存在,而是視作兩個變化維度之間的一個"過渡",原本的"橋"是Abstraction和Implementor之間的組合/聚合關系,而現在DriverManager類本身成為了"橋",可以看作是橋梁模式的一個變體
新的觀點五:變化的維度一個是Driver,一個是Connection
如果從觀點四的原文筆者的角度看,把DriverManager類本身作為"橋",那么我們還可以提出一種新的觀點,繪制類圖如下: