橋接模式( Bridge Pattern ): 可以變化的抽象類與接口

  1. 參考書籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》
  2. Bridge pattern - Wiki

前言

橋接模式是一種提及頻率很高, 應用頻率較少的設計模式。 橋接模式之所以被頻繁提及, 是因爲它的設計意圖提到了“解耦”, 然而它的解耦方式卻常常被很多人誤解。

設計模式用前須知

  • 設計模式種一句出現頻率非常高的話是,“ 在不改動。。。。的情況下, 實現。。。。的擴展“ 。
  • 對於設計模式的學習者來說,充分思考這句話其實非常重要, 因爲這句話往往只對框架/ 工具包的設計纔有真正的意義框架和工具包存在的意義,就是爲了讓其他的程序員使用,在其基礎上實現功能的擴展,而這種功能的擴展必須以不需要改動框架和工具包中代碼爲前提
  • 對於應用程序的編寫者, 從理論上來說, 所有應用層級代碼至少都是處於可編輯範圍內的, 如果不細加考量, 就盲目使用較爲複雜的設計模式, 反而會得不償失, 畢竟靈活性的獲得, 也是有代價的。

橋接模式 ( Bridge )

  • 設計意圖

    • GOF: “ 解耦抽象與實現, 使得二者可以獨立地變化。”
      • 80% 的程序員聽到這句話, 第一反應都是: 一個接口(抽象類)可以對應多個實現類, 通過接口可以解耦抽象與實現。
      • 有趣的是, 第一反應是錯誤的。 原因是設計意圖的後半句話沒有被滿足: “二者可以獨立地變化
        • 當一個接口被定義好以後,實現類的確可以隨意變化, 但是接口可以獨立於實現類隨意變化嗎? 如果你向接口中添加一個方法, 那之前所有實現了該接口的類是否還滿足該接口?
        • 答案顯然是 “不”。
    • GOF舉例 :
      • 考慮一個用於編寫圖形界面的工具包/類庫, 假設該工具包支持編寫可移植的跨平臺界面, 其中定義了一個窗口的抽象(接口/抽象類)Window。 爲了支持X平臺和PM平臺, 需要兩個實現類 XWindow, PMWindow.
      • 到目前爲止, 一切都很好。 可是當你試圖想要擴展Window的抽象時, 就會發現問題。 假設我們要定義一個抽象類 IconWindow, 專用於描述 “圖標窗口” 所需要實現的方法。 此時由於IconWindow也需要支持X平臺和PM平臺, 所以就需要再實現XIConWindow, PMIconWindow。
      • 這裏寫圖片描述
      • 從上述結構可以看出, 每增加一種抽象類abcWindow, 都需要額外實現兩個平臺的實現類 XabcWindow, PMabcWindow 。 這個問題的深層次原因是 : 接口和其實現類是存在耦合關係的, 每當你想要改變接口定義的時候,實現類必須也有相應的變化。
  • 解決方案
    這裏寫圖片描述

圖例說明


在這裏插入圖片描述

這種關係在 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.* 中接口而定義擴展的新接口

總結

理解橋接模式的關鍵在於理解 “橋” 是什麼, “橋”兩側連接的又是什麼。

發佈了51 篇原創文章 · 獲贊 264 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章