一起學設計模式 - 橋接模式

橋接模式(Brideg Pattern)屬於結構型模式的一種,用於把抽象化與實現化解耦,使得二者可以獨立變化,它通過提供抽象化和實現化之間的橋接結構,來實現二者的解耦。

概述

橋接模式是一種很實用的結構型設計模式,如果軟件系統中某個類存在兩個獨立變化的維度,通過該模式可以將這兩個維度分離出來,使兩者可以獨立擴展,讓系統更加符合“單一職責原則”。與多層繼承方案不同,它將兩個獨立變化的維度設計爲兩個獨立的繼承等級結構,並且在抽象層建立一個抽象關聯,該關聯關係類似一條連接兩個獨立繼承結構的橋,故名橋接模式。

橋接模式用一種巧妙的方式處理多層繼承存在的問題,用抽象關聯取代了傳統的多層繼承,將類之間的靜態繼承關係轉換爲動態的對象組合關係,使得系統更加靈活,並易於擴展。

合成複用原則

合成複用原則又稱爲組合/聚合複用原則(Composition/Aggregate Reuse Principle, CARP),指儘量使用對象組合,而不是繼承來達到複用的目的。

爲什麼要儘量使用合成和聚合,而不用繼承

  • 繼承複用破壞包裝,它把父類的實現細節直接暴露給了子類,父類發生改變,則子類也要發生相應的改變,這就直接導致了類之間的緊耦合,不利於類的擴展、複用、維護。
  • 合成與聚合的時候,對象交互往往是通過接口或抽象類進行的,可以很好的避免上面的不足,每個新的類專注於實現自己的任務,符合單一職責原則。

案例

拿汽車在路上行駛的來說。即有小汽車又有公共汽車,它們都不但能在市區中的公路上行駛,也能在高速公路上行駛。這你會發現,對於交通工具(汽車)有不同的類型,然而它們所行駛的環境(路)也在變化,在軟件系統中就要適應兩個方面的變化?怎樣實現才能應對這種變化呢?

緊耦合示例

緊耦合示例

1.定義基類Road,它擁有在上面行駛的方法

class Road {
    public void run() {
        System.out.println("在路上");
    }
}

2.路也有多種,這時候咋整?繼承一波唄

class SpeedWay extends Road {
    @Override
    public void run() {
        System.out.println("在高速公路上");
    }
}

class Street extends Road {
    @Override
    public void run() {
        System.out.println("在市區街道上");
    }
}

3.問題來了,不同的路跑着不同的車,這個時候針對不同的車都要繼承SpeedWay/Street兩個類,假設我有(5種車 * 5條不同的路),媽呀那得多少類,先 (2 * 2)吧

class CarOnSpeedWay extends SpeedWay {
    @Override
    public void run() {
        System.out.println("汽車在高速公路上行駛");
    }
}

class BusOnSpeedWay extends SpeedWay {
    @Override
    public void run() {
        System.out.println("公共汽車在高速公路上行駛");
    }
}


class CarOnStreet extends Street {
    @Override
    public void run() {
        System.out.println("汽車在街道上行駛");
    }
}

class BusOnStreet extends Street {
    @Override
    public void run() {
        System.out.println("公共汽車在街道上行駛");
    }
}

4.編寫測試類,有木有發現代碼囉嗦不說,不是說好的一般是面向接口編程麼?

public class Client {

    public static void main(String[] args) {
        CarOnSpeedWay carOnSpeedWay = new CarOnSpeedWay();
        carOnSpeedWay.run();

        BusOnSpeedWay busOnSpeedWay = new BusOnSpeedWay();
        busOnSpeedWay.run();

        CarOnStreet carOnStreet = new CarOnStreet();
        carOnStreet.run();

        BusOnStreet busOnStreet = new BusOnStreet();
        busOnStreet.run();
    }
}

弊端: 仔細分析就可以發現,該方案雖然實現了功能但埋了不少坑,它在遵循開放-封閉原則的同時,違背了類的單一職責原則,即一個類只有一個引起它變化的原因,而這裏引起變化的原因卻有兩個,即路類型的變化和汽車類型的變化;其次是重複代碼會很多,不同的汽車在不同的路上行駛也會有一部分的代碼是相同的;再次是類的結構過於複雜,繼承關係太多,難於維護,最後最致命的一點是擴展性太差。如果變化沿着汽車的類型和不同的道路兩個方向變化,我們會看到這個類的結構會迅速的變龐大。

鬆耦合示例

首先我們要搞清楚,路上面可以行駛車輛(包含),但車並不是路的一部分,它們二者並不一定要緊密貼在一塊,它們之間是聚合關係

鬆耦合示例

1.定義一個Car的接口,供給不同種類的去實現

interface Car {
    void run();
}

2.抽象類AbstractRoad,與Car關聯起來,接收具體的實現

abstract class AbstractRoad {
    protected Car car;

    public void setCar(Car car) {
        this.car = car;
    }
    public abstract void run();
}

3.道路類統一繼承AbstractRoad實現抽象方法,汽車類統一實現Car接口

class SpeedWay extends AbstractRoad {
    @Override
    public void run() {
        car.run();
        System.out.println("在高速公路上");
    }
}

class Street extends AbstractRoad {
    @Override
    public void run() {
        car.run();
        System.out.println("在市區街道上");
    }
}

class SmallCar implements Car {
    @Override
    public void run() {
        System.out.println("小汽車");
    }
}
class BigTruck implements Car {
    @Override
    public void run() {
        System.out.println("大卡車");
    }
}

4.編寫測試類,通過傳入具體實現來獲得最終結果

public class Client {

    public static void main(String[] args) {
        Car bigTruck = new BigTruck();
        Car smallCar = new SmallCar();
        AbstractRoad way = new SpeedWay();
        way.setCar(bigTruck);
        way.run();
        way.setCar(smallCar);
        way.run();

        AbstractRoad street = new Street();
        street.setCar(bigTruck);
        street.run();
        street.setCar(smallCar);
        street.run();
    }
}

可以看到,通過對象組合的方式,Bridge 模式把兩個角色之間的繼承關係改爲了聚合的關係,從而使這兩者可以從容自若的各自獨立的變化,這也是Bridge模式的本意。

多維度變化

結合上面的例子,增加一個維度”人”,不同的人開着不同的汽車在不同的路上行駛(三個維度);結合上面增加一個類”人”,並重新調用.

多維度變化

abstract class People {

    protected AbstractRoad abstractRoad;

    public void setAbstractRoad(AbstractRoad abstractRoad) {
        this.abstractRoad = abstractRoad;
    }

    public abstract void run();
}

class Man extends People {

    @Override
    public void run() {
        System.out.println("男人開着");
        abstractRoad.run();
    }
}

public class Client {

    public static void main(String[] args) {
        Car bigTruck = new BigTruck();
        People people = new Man();
        AbstractRoad street = new Street();
        street.setCar(bigTruck);
        people.setAbstractRoad(street);
        people.run();
    }
}

JDBC與橋接

JDBC與橋接類圖

通常使用JDBC連接數據庫時,首選就是加載數據庫驅動,然後獲取數據庫連接

Class.forName("數據庫類驅動器");
Connection conn = DriverManager.getConnection("數據庫url", "用戶名", "密碼");
//.................

針對不同的數據庫,JDBC都可以通過java.sql.DriverManager類的靜態方法getConnection(數據庫url, 用戶名, 密碼)來獲取數據庫的連接。JDBC通過DriverManager對外提供了操作數據庫的統一接口getConnection,通過該方法可以獲取不同數據庫的連接,並且通過Connection類提供的接口來進行數據的查詢操作。

JDBC爲不同的數據庫操作提供了相同的接口,但是JDBC本身並沒有針對每種數據庫提供一套具體實現代碼,而是通過接口java.sql.Driver的connect方法連接到了不同的數據庫實現。

public interface Driver {

    Connection connect(String url, java.util.Properties info) throws SQLException;
    //其他方法
}

在JDBC中,針對不同數據庫提供的統一的操作接口通過java.sql.Driver(橋)連接到了不同的數據庫實現。如連接mysql數據庫。

package com.mysql.jdbc;

public class NonRegisteringDriver implements java.sql.Driver { 

    public java.sql.Connection connect(String url, Properties info) throws SQLException {
        //省略具體實現代碼...
    }

    //其他方法
}

具體實現

1.在com.mysql.jdbc.Driver的源碼中可以看到,類加載時會將Driver註冊到DriverManager中。

package com.mysql.jdbc;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

2.在DriverManager中,調用getConnection會迭代驅動註冊表中的驅動,然後調用Driver接口提供的connect方法來獲取Connection對象

public class DriverManager {

    // JDBC驅動程序註冊表
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();


    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);

    }

    private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
            ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
            synchronized(DriverManager.class) {
                // synchronize loading of the correct classloader.
                if (callerCL == null) {
                    callerCL = Thread.currentThread().getContextClassLoader();
                }
            }

            if(url == null) {
                throw new SQLException("The url cannot be null", "08001");
            }

            println("DriverManager.getConnection(\"" + url + "\")");

            SQLException reason = null;
            //遍歷registeredDrivers表
            for(DriverInfo aDriver : registeredDrivers) {
                // 如果沒有加載驅動的權限則跳過
                if(isDriverAllowed(aDriver.driver, callerCL)) {
                    try {
                        //調用Driver接口提供的connect方法來獲取Connection對象
                        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());
                }

            }

            // if we got here nobody could connect.
            if (reason != null)    {
                println("getConnection failed: " + reason);
                throw reason;
            }
            println("getConnection: no suitable driver found for "+ url);
            throw new SQLException("No suitable driver found for "+ url, "08001");
        }
    }
}

上述代碼中爲橋接模式在JDBC中運用方式

總結

實現要點

  • Bridge模式使用對象間的組合關係解耦了抽象和實現之間固有的綁定關係,使得抽象和實現可以沿着各自的維度來變化。
  • 所謂抽象和實現沿着各自維度的變化,即子類化它們,得到各個子類之後,便可以任意它們,從而獲得不同路上的不同汽車。
  • Bridge模式有時候類似於多繼承方案,但是多繼承方案往往違背了類的單一職責原則(即一個類只有一個變化的原因),複用性比較差。Bridge模式是比多繼承方案更好的解決方法。
  • Bridge模式的應用一般在兩個非常強的變化維度,有時候,即使兩個維度存在變化,但是變化的地方影響並不大,換而言之兩個變化不會導致縱橫交錯的結果,並不一定要使用Bridge模式。

適用場景

  1. 如果一個系統需要在構件的抽象化角色和具體化角色之間增加更多的靈活性,避免在兩個層次之間建立靜態的聯繫。
  2. 設計要求實現化角色的任何改變不應當影響客戶端,或者說實現化角色的改變對客戶端是完全透明的。
  3. 一個構件有多於一個的抽象化角色和實現化角色,系統需要它們之間進行動態耦合。
  4. 雖然在系統中使用繼承是沒有問題的,但是由於抽象化角色和具體化角色需要獨立變化,設計要求需要獨立管理這兩者。

相關模式

裝飾模式橋接模式在一定程度上都是爲了減少子類的數目,避免出現複雜的繼承關係。但是它們解決的方法卻各有不同,裝飾模式把子類中比基類中多出來的部分放到單獨的類裏面,以適應新功能增加的需要,當我們把描述新功能的類封裝到基類的對象裏面時,就得到了所需要的子類對象,這些描述新功能的類通過組合可以實現很多的功能組合.

- 說點什麼

參考文獻:http://blog.csdn.net/zlts000/article/details/26749723
參考文獻:https://www.cnblogs.com/houleixx/archive/2008/02/23/1078877.html
參考文件:http://www.cnblogs.com/-crazysnail/p/3977815.html

全文代碼:https://gitee.com/battcn/design-pattern/tree/master/Chapter6/battcn-brideg

  • 個人QQ:1837307557
  • battcn開源羣(適合新手):391619659

微信公衆號:battcn(歡迎調戲)

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