設(shè)計模式系列—橋接模式
本篇和大家一起來學(xué)習(xí)橋接模式相關(guān)內(nèi)容。
模式定義
將抽象與實現(xiàn)分離,使它們可以獨立變化。它是用組合關(guān)系代替繼承關(guān)系來實現(xiàn),從而降低了抽象和實現(xiàn)這兩個可變維度的耦合度。
模式實現(xiàn)如下:
- package com.niuh.designpattern.bridge.v1;
- /**
- * 橋接模式
- */
- public class BridgePattern {
- public static void main(String[] args) {
- Implementor imple=new ConcreteImplementorA();
- Abstraction abs=new RefinedAbstraction(imple);
- abs.Operation();
- }
- }
- //實現(xiàn)化角色
- interface Implementor {
- void OperationImpl();
- }
- //具體實現(xiàn)化角色
- class ConcreteImplementorA implements Implementor {
- public void OperationImpl() {
- System.out.println("具體實現(xiàn)化(Concrete Implementor)角色被訪問");
- }
- }
- //抽象化角色
- abstract class Abstraction {
- protected Implementor imple;
- protected Abstraction(Implementor imple) {
- this.imple = imple;
- }
- public abstract void Operation();
- }
- //擴展抽象化角色
- class RefinedAbstraction extends Abstraction {
- protected RefinedAbstraction(Implementor imple) {
- super(imple);
- }
- public void Operation() {
- System.out.println("擴展抽象化(Refined Abstraction)角色被訪問");
- imple.OperationImpl();
- }
- }
輸出結(jié)果如下:
- 擴展抽象化(Refined Abstraction)角色被訪問
- 具體實現(xiàn)化(Concrete Implementor)角色被訪問
解決的問題
在有多種可能會變化的情況下,用繼承會造成類爆炸問題,擴展起來不靈活。
模式組成
可以將抽象化部分與實現(xiàn)化部分分開,取消二者的繼承關(guān)系,改用組合關(guān)系。
實例說明
實例概況
某公司開發(fā)了一個財務(wù)管理系統(tǒng),其中有個報表生成器的工具模塊,客戶可以指定任意一種報表類型,如基本報表,往來報表,資金報表,資產(chǎn)報表等,并且可以指定不同 的報表樣式,如餅圖,柱狀圖等。系統(tǒng)設(shè)計人員針對這個報表生成器的結(jié)構(gòu)設(shè)計了如下圖所示的類圖。
后來在客戶使用過程中,客戶又希望增加一個新的報表和新的線形圖,開發(fā)人員這個時候發(fā)現(xiàn)維護起來非常麻煩,設(shè)計人員經(jīng)過仔細分析,發(fā)現(xiàn)存在嚴(yán)重的問題,因為新增加一個報表或者圖,需要增加很多子類。所以,系統(tǒng)分析師最終對這個模塊根據(jù)面向?qū)ο蟮脑O(shè)計原則對上面的方案進行了重構(gòu),重構(gòu)后的圖如下所示。
在本重構(gòu)方案中,將報表和圖形設(shè)計成兩個繼承結(jié)構(gòu),兩者都可以獨立變化,編程的時候可以只針對抽象類編碼,而在運行的時候再將具體的圖形子類對象注入到具體的 報表類中。這樣的話,系統(tǒng)就具有良好的可擴展性和可維護性,并且滿足了面向?qū)ο笤O(shè)計原則的開閉原則。
使用步驟
步驟1:定義實現(xiàn)化角色,報表接口
- interface IReport {
- void operationImpl();
- }
步驟2:定義具體實現(xiàn)化角色(基本報表、往來報表、資金報表)
- class BasicReport implements IReport {
- @Override
- public void operationImpl() {
- System.out.println("基本報表被訪問.");
- }
- }
- class IntercourseReport implements IReport {
- @Override
- public void operationImpl() {
- System.out.println("往來報表被訪問.");
- }
- }
- class CapitalReport implements IReport {
- @Override
- public void operationImpl() {
- System.out.println("資金報表被訪問.");
- }
- }
步驟3:定義抽象化角色,圖形
- abstract class AbstractionGraph {
- protected IReport iReport;
- public AbstractionGraph(IReport iReport) {
- this.iReport = iReport;
- }
- abstract void operation();
- }
步驟4:定義擴展抽象化角色(柱狀圖、餅圖)
- class Barchart extends AbstractionGraph {
- public Barchart(IReport iReport) {
- super(iReport);
- }
- @Override
- void operation() {
- System.out.println("柱狀圖被訪問.");
- iReport.operationImpl();
- }
- }
- class Piechart extends AbstractionGraph {
- public Piechart(IReport iReport) {
- super(iReport);
- }
- @Override
- void operation() {
- System.out.println("餅圖被訪問.");
- iReport.operationImpl();
- }
- }
步驟5:測試
- public class BridgePattern {
- public static void main(String[] args) {
- //實現(xiàn)化和抽象化分離
- // 基本報表
- IReport basicReport = new BasicReport();
- // 往來報表
- IReport intercourseReport = new IntercourseReport();
- // 資金報表
- IReport capitalReport = new CapitalReport();
- // 基本報表使用柱狀圖
- AbstractionGraph barchart = new Barchart(basicReport);
- barchart.operation();
- // 基本報表使用餅圖
- AbstractionGraph piechart = new Piechart(basicReport);
- piechart.operation();
- }
- }
輸出結(jié)果
- 柱狀圖被訪問.
- 基本報表被訪問.
- 餅圖被訪問.
- 基本報表被訪問.
優(yōu)點
橋接模式遵循了里氏替換原則和依賴倒置原則,最終實現(xiàn)了開閉原則,對修改關(guān)閉,對擴展開放。這里將橋接模式的優(yōu)缺點總結(jié)如下。
橋接(Bridge)模式的優(yōu)點:
- 抽象與實現(xiàn)分離,擴展能力強
- 符合開閉原則
- 符合合成復(fù)用原則
- 其實現(xiàn)細節(jié)對客戶透明
缺點
由于聚合關(guān)系建立在抽象層,要求開發(fā)者針對抽象化進行設(shè)計與編程,能正確地識別出系統(tǒng)中兩個獨立變化的維度,這增加了系統(tǒng)的理解與設(shè)計難度。
應(yīng)用場景
當(dāng)一個類內(nèi)部具備兩種或多種變化維度時,使用橋接模式可以解耦這些變化的維度,使高層代碼架構(gòu)穩(wěn)定。
橋接模式通常適用于以下場景:
- 當(dāng)一個類存在兩個獨立變化的維度,且這兩個維度都需要進行擴展時;
- 當(dāng)一個系統(tǒng)不希望使用繼承或因為多層次繼承導(dǎo)致系統(tǒng)類的個數(shù)急劇增加時;
- 當(dāng)一個系統(tǒng)需要在構(gòu)件的抽象化角色和具體化角色之間增加更多的靈活性時。
橋接模式的一個常見使用場景就是替換繼承。我們知道,繼承擁有很多優(yōu)點,比如,抽象、封裝、多態(tài)等,父類封裝共性,子類實現(xiàn)特性。繼承可以很好的實現(xiàn)代碼復(fù)用(封裝)的功能,但這也是繼承的一大缺點。
因為父類擁有的方法,子類也會繼承得到,無論子類需不需要,這說明繼承具備強侵入性(父類代碼侵入子類),同時會導(dǎo)致子類臃腫。因此,在設(shè)計模式中,有一個原則為優(yōu)先使用組合/聚合,而不是繼承。
橋接模式模式的擴展
在軟件開發(fā)中,有時橋接(Bridge)模式可與適配器模式聯(lián)合使用。當(dāng)橋接(Bridge)模式的實現(xiàn)化角色的接口與現(xiàn)有類的接口不一致時,可以在二者中間定義一個適配器將二者連接起來,其結(jié)構(gòu)圖如下:
源碼中的應(yīng)用
- JDBC驅(qū)動程序
- ......
DriverManager類
DriverManager作為一個抽象化角色,聚合了實現(xiàn)化角色Connection,只不過與標(biāo)準(zhǔn)的橋梁模式不一樣的是,DriverManager類下面沒有子類。
- // Worker method called by the public getConnection() methods.
- 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");
- }
PS:以上代碼提交在 Github :
https://github.com/Niuh-Study/niuh-designpatterns.git