- 參考書籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》
- Bridge pattern - Wiki
前言
橋接模式是一種提及頻率很高, 應用頻率較少的設計模式。 橋接模式之所以被頻繁提及, 是因爲它的設計意圖提到了“解耦”, 然而它的解耦方式卻常常被很多人誤解。
設計模式用前須知
- 設計模式種一句出現頻率非常高的話是,“ 在不改動。。。。的情況下, 實現。。。。的擴展“ 。
- 對於設計模式的學習者來說,充分思考這句話其實非常重要, 因爲這句話
往往只對框架/ 工具包的設計纔有真正的意義
。框架和工具包存在的意義,就是爲了讓其他的程序員使用,在其基礎上實現功能的擴展,而這種功能的擴展必須以不需要改動框架和工具包中代碼爲前提
- 對於應用程序的編寫者, 從理論上來說, 所有應用層級代碼至少都是處於可編輯範圍內的, 如果不細加考量, 就盲目使用較爲複雜的設計模式, 反而會得不償失, 畢竟靈活性的獲得, 也是有代價的。
橋接模式 ( Bridge )
-
設計意圖
- GOF: “ 解耦抽象與實現, 使得二者可以獨立地變化。”
- 80% 的程序員聽到這句話, 第一反應都是: 一個接口(抽象類)可以對應多個實現類, 通過接口可以解耦抽象與實現。
- 有趣的是,
第一反應是錯誤的
。 原因是設計意圖的後半句話沒有被滿足: “二者可以獨立地變化”當一個接口被定義好以後,實現類的確可以隨意變化, 但是接口可以獨立於實現類隨意變化嗎?
如果你向接口中添加一個方法, 那之前所有實現了該接口的類是否還滿足該接口?- 答案顯然是 “不”。
- GOF舉例 :
- 考慮一個用於編寫圖形界面的工具包/類庫, 假設該工具包支持編寫可移植的跨平臺界面, 其中定義了一個窗口的抽象(接口/抽象類)Window。 爲了支持X平臺和PM平臺, 需要兩個實現類 XWindow, PMWindow.
- 到目前爲止, 一切都很好。 可是當你試圖想要擴展Window的抽象時, 就會發現問題。 假設我們要定義一個抽象類 IconWindow, 專用於描述 “圖標窗口” 所需要實現的方法。 此時由於IconWindow也需要支持X平臺和PM平臺, 所以就需要再實現XIConWindow, PMIconWindow。
- 從上述結構可以看出,
每增加一種抽象類abcWindow, 都需要額外實現兩個平臺的實現類 XabcWindow, PMabcWindow
。 這個問題的深層次原因是 : 接口和其實現類是存在耦合關係的, 每當你想要改變接口定義的時候,實現類必須也有相應的變化。
- GOF: “ 解耦抽象與實現, 使得二者可以獨立地變化。”
-
解決方案
圖例說明
這種關係在 java 語言中, 可以按照如下形式實現(不止這一種實現形式)
abstract class A {
private B b;
}
注意: A, B 的類型比較靈活, 可以是 class, abstract class , interface 的任意一種類型, 使用哪種完全看需求
這種關係在 java 語言中, 可以按照如下形式實現
class A extends B{
}
或
class A implements B{
}
橋接模式解析
- 注意點1:Window 定義的方法是有方法體的, Window 中定義的繪製矩形 DrawRect 操作, 都是通過調用 impl 的方法實現的。 (這並不意味着 Window 只能是一個抽象類, Java8 是支持在接口中定義方法體的喲)
- 注意點2: WindowImpl 也是一個抽象類( 接口), 並非一個具體的實現類
- 注意點3: Window 的子抽象類(子接口)中定義的新方法,都是通過調用 WindowImpl 中的方法或調用 Window 中的方法間接定義的。
- 這一點是最容易被忽略的: 如果 Window 的子接口中,直接增加一個新的方法 DrawCircle(), 搭在Window 和 WindowImpl 之間“橋” 就失效了。 橋右端的實現類中, 是無法間接地實現DrawCircle 這個新增的方法的。
- 橋接模式的設計意圖雖然是讓 “抽象部分“和“實現部分“ 能夠獨立變化,但經過上述分析不難發現, 抽象接口的變化還是有所限制的, 並不能隨意變化。
- 下面這張圖更加形象地描述了橋接模式中重定義的抽象類(RedefinedAbstraction)中的 Composite Operations 對於基礎抽象類中(Abstraction)中的 Abstract Operations 的依賴關係
- 橋接模式泛化結構圖
橋接模式與抽象工廠
- 上述的例子裏, 提到了跨平臺的問題, 回想一下可以發現, 抽象工廠模式中所舉的例子, 出發點也是跨平臺, 那兩者這件是否有所關聯? 答案是肯定的。
- 還是以之前的例子進行說明, 注意到基礎抽象類 Window 中持有了一個 WindowImp 的引用imp , 如果 Window 可以預先確定要支持的只有X平臺和PM平臺, 就可以在 Window 構造器中通過參數來指定實例化哪一種 WindowImpl.
public Window ( String type )
{
switch(type){
case "X"
this.imp = new XWindowImp();
break;
case "PW"
this.imp = new PMWindowImpl();
break;
}
}
如果Window中無法預先確定有哪些實現類, 想給用戶提供擴展支持平臺的機會, 那麼就可以利用抽象工廠模式來實例化impl 對象
public Window ( AbstractFactory factory )
{
this.impl = factory.createWindowImpl()
}
通過傳入用戶自定義的新的Factory實現類 , 就可以擴展Window所支持的平臺。
- 綜上, 抽象工廠模式可以用來搭建或者配置橋接模式中的 “橋樑” 。
橋接模式的真實應用案例 JDBC
jdbc 作爲 jdk 提供的數據庫訪問 api, 顯然就需要實現一套平臺無關的通用代碼, 支持各種不同數據庫的訪問(oracle, mysql, PostgreSQL, Microsoft Sql Server…)
這種需求就通過橋接模式進行了有效解決。
首先看一下 jdbc 的基本使用流程。
- 根據數據庫類型, 選擇對應的驅動類進行加載:
//加載MySql驅動
Class.forName("com.mysql.jdbc.Driver")
//加載Oracle驅動
//Class.forName("oracle.jdbc.driver.OracleDriver")
- 獲得數據庫連接:
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase", "root", "root");
- 創建Statement 或 PreparedStatement對象:
//conn.createStatement();
conn.prepareStatement(sql);
現在可以分析一下, JDBC 是如何應用橋接模式的。
再次回顧一下橋接模式的泛化結構圖, 以此爲基礎分析 JDBC 是如何使用該模式的
- 橋接模式泛化結構圖
值得注意的是,java.sql.*
中的衆多接口和類裏,java.sql.DriverManager
接口是橋接模式應用的出發點, 這個DriverManager
接口就對應於上圖中Abstraction
, 而imp
則對應於registeredDrivers
public class DriverManager {
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
= new CopyOnWriteArrayList<>();
// 省略
進而可以看出, 上圖中的 Implementor
好像對應於 DriverInfo
, 於是查看 DriverInfo
源碼, 發現DriverInfo
並不是一個接口或者抽象類, 它只是簡單地封裝了有2個的成員變量 Driver, DriverAction
,
class DriverInfo {
final Driver driver;
DriverAction da;
DriverInfo(Driver driver, DriverAction action) {
this.driver = driver;
da = action;
}
@Override
public boolean equals(Object other) {
return (other instanceof DriverInfo)
&& this.driver == ((DriverInfo) other).driver;
}
@Override
public int hashCode() {
return driver.hashCode();
}
@Override
public String toString() {
return ("driver[className=" + driver + "]");
}
DriverAction action() {
return da;
}
}
查看一下發現 Driver, DriverAction
的定義是 Interface
, 由於 DriverAction 的引用在 DriverManager 中並未調用, 所以可以判斷 Driver
對應於橋接模式中的 Implementor
, 。
可以看出,不同的數據庫廠商提供的驅動包, 首先必須實現 Driver, DriverAction
定義的接口, 另外還需要實現 Driver, DriverAction
中所進一步引用的接口。 以 Driver
中的一個關鍵方法 connect(url, info)
爲例, 由於該方法的返回值被定義爲 java.sql.Connection
, 數據庫驅動包中,則必然需要體用 Connection
對應的實現
public interface Driver {
// ... 代碼略 ...
Connection connect(String url, java.util.Properties info)
throws SQLException;
// ... 代碼略 ...
}
現在理清了如下線索:
- 橋接模式中的 Abstraction -->
DriverManager
- 橋接模式中的 Implementor -->
Driver
還需解決的問題:
- 橋接模式中的 Operation --> ?
- 橋接模式中的 RefinedAbstrction --> ?
Operation 對應的定義比較容易發現, 因爲橋接模式中的 Operation 必然會調用 Implementor
, 此時再次查看 DriverManager
的源碼, 查找使用了 registeredDrivers
的地方, 發現如下方法
private static Connection getConnection(
// ... 省略部分代碼 ...
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());
}
}
// ... 省略部分代碼 ...
}
getConnection
方法中,在已經註冊的 registeredDrivers
中,遍歷調用 connect
方法, 嘗試建立數據庫連接,建立成功則予以返回。
這樣就明確了:
- 橋接模式中的 Operation -->
DriverManager @ getConnection
剩下的問題就是 RefinedAbstraction 對應於什麼?
可能會有人認爲 RefinedAbstract 對應於 java.sql.Connection
接口中, 會用到的 Statement
等同在 java.sql.*
中的接口
但是觀察橋接模式的泛化結構圖就會發現, 橋接模式所定義的 RefinedAbstraction
中所定義的接口, 與 Abstraction
所對應的接口, 應該是繼承關係。 且 RefinedAbstraction 中定義的方法中,會對 Abstraction 接口中的方法進行調用。
從 DriverManager 出發進行查找, 發現 jdk 中並沒有類繼承 DriverManager。
思考: 這是否意味着橋接 JDBC 並未完整的應用橋接模式。
答案: 否
前面提到過, 由於 getConnection() 方法的返回類型爲 java.sql.Connection
, 且該方法調用了 driver.connect(url,info)
來獲取 Connection
, 由於 Driver
類的角色是橋接模式中的 Implementor, 這就保確保了 Connection
接口的實現以及 Connection 接口中, 進一步定義和引用的接口, 都會由數據庫驅動商提供,這就確保了 java.sql.*
中的各類接口都會由對應的數據庫驅動提供實現。
進一步的, 對於使用 JDBC 的程序員, 可以繼承 java.sql.*
的接口, 自定義一些數據庫訪問接口,而這種變化, 與 Driver 的變化就相互獨立, 各不影響了。
總結橋接模式在 JDBC 中使用的對應關係:
- 橋接模式中的 Abstraction -->
DriverManager
, 以及傳遞引用到的 java.sql.* 中的Connection
等其他接口- 橋接模式中的 Implementor -->
Driver
- 橋接模式中的 Operation -->
DriverMaanager#getConnection()
- 橋接模式中的 RefinedAbstrction --> 程序員自行繼承
java.sql.*
中接口而定義擴展的新接口
總結
理解橋接模式的關鍵在於理解 “橋” 是什麼, “橋”兩側連接的又是什麼。