設計模式之橋接模式

2022-08-05 18:01:06

本文通過老王和小王買車,引出設計模式中的結構型設計之橋接模式,接著說明設計型模式的概念和程式碼實現,為了加深理解,會說明介面卡設計模式在JDBC中的應用,最後談談橋接模式和介面卡模式的總結。

讀者可以拉取完整程式碼到本地進行學習,實現程式碼均測試通過後上傳到碼雲

一、引出問題

老王和小王去賓士4S店買車,賓士4S店的各種品牌型號琳琅滿目,老王想試駕賓士E、小王想試駕賓士G,並且提出兩種賓士型號的各種顏色都想體驗一把,這讓店小二犯了難,兩兩組合就是很多種,4S店壓根放不下。

無奈店小二求救經理,經理出了一個注意:將賓士E和G開的品牌抽象出來,將顏色也抽象出來,通過品牌和顏色的組合代替繼承關係,減少了顏色和品牌的耦合,且減少了車的個數,只需要兩臺就夠了。

果然經理不愧是經理。

經理所說的其實就是橋接模式。這種模式涉及到一個作為橋接的介面,使得實體類的功能獨立於介面實現類。這兩種型別的類可被結構化改變而互不影響。

二、概念與使用

我們看一些概念:橋接(Bridge)是用於把抽象化與實現化解耦,使得二者可以獨立變化。這種型別的設計模式屬於結構型模式,它通過提供抽象化和實現化之間的橋接結構,來實現二者的解耦。

在該模式中應該涉及到四個角色:

①實現類介面(Implementor):定義實現角色的介面,供擴充套件抽象化角色使用,例如抽象出奔馳品牌benz 可以擴充套件出 benzE benzG

②具體實現角色(ConcreteImplementor):實現類的具體實現,例如各種賓士品牌

③抽象化(Abstraction)角色:定義一個抽象類,其中參照了實現化角色(想要組合),例如汽車產品

④擴充套件抽象化(RefinedAbstraction)角色:抽象化角色子類,實現父類別方法,且通過組合關係呼叫實現化角色中的業務方法,例如具體賓士產品,紅色賓士、白色賓士

根據該模式的定義,我們將賓士品牌抽象出來,然後各品牌有各自的實現,每個顏色的車把車品牌組合進來,在使用者端中每個相機型別和相機品牌都能兩兩組合。

我們看具體的程式碼實現:

實現類介面:

/**
 * 賓士品牌類
 * @author tcy
 * @Date 05-08-2022
 */
public interface BenzBrand {
    void showInfo();
}

具體實現角色1:

/**
 * @author tcy
 * @Date 05-08-2022
 */
public class BenzE implements BenzBrand{
    @Override
    public void showInfo() {
        System.out.print("【賓士E】顏色是:");
    }
}

具體實現角色2:

/**
 * @author tcy
 * @Date 05-08-2022
 */
public class BenzG implements BenzBrand{
    @Override
    public void showInfo() {
        System.out.print("【賓士G】顏色是:");

    }
}

抽象化角色:

/**
 * 抽象賓士類
 * @author tcy
 * @Date 05-08-2022
 */
public abstract class Benz {
    // 將品牌組合進來
    protected BenzBrand benzBrand;

    public Benz(BenzBrand benzBrand) {
        this.benzBrand = benzBrand;
    }

    public void showInfo(){
        benzBrand.showInfo();
    }


}

擴充套件抽象化1:

/**
 * @author tcy
 * @Date 05-08-2022
 */
public class BlackBenz extends Benz {
    public BlackBenz(BenzBrand benzBrand) {
        super(benzBrand);
    }

    @Override
    public void showInfo() {
        super.showInfo();

        System.out.println("黑色...");
    }


}

擴充套件抽象化2:

/**
 * @author tcy
 * @Date 05-08-2022
 */
public class RedBenz extends Benz {
    public RedBenz(BenzBrand benzBrand) {
        super(benzBrand);
    }
    @Override
    public void showInfo() {
        super.showInfo();
        System.out.println("紅色...");
    }

}

使用者端呼叫:

/**
 * @author tcy
 * @Date 05-08-2022
 */
public class Client {

    public static void main(String[] args) {
        // 黑色賓士E
        Benz benz1 = new BlackBenz(new BenzE());
        benz1.showInfo();
        // 黑色賓士G
        Benz benz2 = new BlackBenz(new BenzG());
        benz2.showInfo();
        // 紅色賓士E
        Benz benz3 = new RedBenz(new BenzE());
        benz3.showInfo();
        // 紅色賓士G
        Benz benz4 = new RedBenz(new BenzG());
        benz4.showInfo();
    }
【賓士E】顏色是:黑色...
【賓士G】顏色是:黑色...
【賓士E】顏色是:紅色...
【賓士G】顏色是:紅色...

這樣即使老王提出來新的顏色、新的車型,只需要增加相應的具體實現角色或者擴充套件抽象化角色即可。

顧名思義,橋接模式就像是一個橋,可以用來連線兩個不同地方,這兩個地方自由發展,中間的貿易是通過一座橋來連線。

這種方法的缺點也很顯著,汽車能很快的確立型號和顏色兩個維度,在實際業務開發中,識別出系統兩個獨立變化的維度就不簡單了。

不難看出,列舉的例子有些過於強求,在現實世界中是永遠不可能發生的,為了加深理解我找了大量在JDK亦或是Spirng等各種框架對橋接模式的應用,只找到了橋接模式在Jdbc中的應用。

三、應用

我們都知道通過JDBC可以完成Java對關係型資料庫的SQL操作,我們在連線資料資料庫時,想必都接觸過Driver,在連線MySQL和Oracle的Driver都是不同的,這些都是實現介面類。

我們看一下MySQL中實現的Driver類。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

在該類中實際上有兩個作用,一是呼叫了DriverManager中的registerDriver方法來註冊驅動,二是當驅動註冊完成後,我們就會開始呼叫DriverManager中的getConnection方法了。

我們看DriverManager的完整程式碼:

    public class DriverManager {
    
    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()));
}

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    /*
     * When callerCl is null, we should check the application's
     * (which is invoking this class indirectly)
     * classloader, so that the JDBC driver class outside rt.jar
     * can be loaded from here.
     */
    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 + "\")");

    // Walk through the loaded registeredDrivers attempting to make a connection.
    // Remember the first exception that gets raised so we can reraise it.
    SQLException reason = null;

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

    }

    // 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");
}
}
}

在Java中通過Connection提供給各個資料庫一樣的操作介面,這裡的Connection可以看作抽象類。

可以說我們用來操作不同資料庫的方法都是相同的,不過MySQL有自己的ConnectionImpl類,同樣Oracle也有對應的實現類。

這裡Driver和Connection之間是通過DriverManager類進行橋接的,這種橋接模式和我們上面可以清晰的看出來各個角色是不同的。

四、總結

橋接模式是很好理解的,相信認真看了範例的同學應該都可以看懂,但那並不代表你已經掌握了該設計模式。在我們使用JDBC的時候,想必有很多同學並不能看出來這是橋接模式。

紙上得來終覺淺,有一部分例子是為了說明橋接模式而「構想」出來的,各個角色都是清晰直觀。看了這樣的程式碼,可以學會橋接模式,但是到了實際中很可能還是不會用。

最好的方法就是給出真實專案裡的例子。但是這個難度確實很大,一到了真實專案裡,就會遇到很多細節問題,從而影響對模式的理解,而且真實專案都帶有一定的業務環境。

看懂並且學會了設計模式是一回事,在實際開發中擇優選擇設計模式那是另外一回事,這不僅需要對各個設計模式理解到位,更多的是對業務的理解和程式碼理念的把控。

推薦讀者,參考軟體設計七大原則 認真閱讀往期的文章,認真體會。

建立型設計模式

一、設計模式之工廠方法和抽象工廠

二、設計模式之單例和原型

三、設計模式之建造者模式

結構型設計模式

四、設計模式之代理模式

五、設計模式之介面卡模式