【白話設計模式九】橋接模式(Bridge)

#0 系列目錄#算法

#1 場景問題# ##1.1 發送提示消息## 考慮這樣一個實際的業務功能:發送提示消息。基本上全部帶業務流程處理的系統都會有這樣的功能,好比某人有新的工做了,須要發送一條消息提示他。sql

從業務上看,消息又分紅普通消息、加急消息和特急消息多種,不一樣的消息類型,業務功能處理是不同的,好比加急消息是在消息上添加加急,而特急消息除了添加特急外,還會作一條催促的記錄,多久不完成會繼續催促。從發送消息的手段上看,又有系統內短消息、手機短消息、郵件等等。數據庫

如今要實現這樣的發送提示消息的功能,該如何實現呢?編程

##1.2 不用模式的解決方案##設計模式

  1. 實現簡化版本

先考慮實現一個簡單點的版本,好比:消息先只是實現發送普通消息,發送的方式呢,先實現系統內短消息和郵件。其它的功能,等這個版本完成事後,再繼續添加,這樣先把問題簡單化,實現起來會容易一點。架構

(1)因爲發送普通消息會有兩種不一樣的實現方式,爲了讓外部能統一操做,所以,把消息設計成接口,而後由兩個不一樣的實現類,分別實現系統內短消息方式和郵件發送消息的方式。此時系統結構如圖1所示:學習

輸入圖片說明

(2)先來看看消息的統一接口,示例代碼以下:測試

/** 
 * 消息的統一接口 
 */  
public interface Message {  
    /** 
     * 發送消息 
     * @param message 要發送的消息內容 
     * @param toUser 消息發送的目的人員 
     */  
    public void send(String message,String toUser);  
}

(3)再來分別看看兩種實現方式,這裏只是爲了示意,並不會真的去發送Email和站內短消息,先看站內短消息的方式,示例代碼以下:ui

/** 
 * 以站內短消息的方式發送普通消息 
 */  
public class CommonMessageSMS implements Message {  
    public void send(String message, String toUser) {  
        System.out.println("使用站內短消息的方式,發送消息'"+message+"'給"+toUser);  
    }  
}

/** 
 * 以Email的方式發送普通消息 
 */  
public class CommonMessageEmail implements Message {  
    public void send(String message, String toUser) {  
        System.out.println("使用Email的方式,發送消息'"+message+"'給"+toUser);  
    }  
}
  1. 實現發送加急消息

上面的實現,看起來很簡單,對不對。接下來,添加發送加急消息的功能,也有兩種發送的方式,一樣是站內短消息和Email的方式。this

加急消息的實現跟普通消息不一樣,加急消息會自動在消息上添加加急,而後再發送消息;另外加急消息會提供監控的方法,讓客戶端能夠隨時經過這個方法來了解對於加急消息處理的進度,好比:相應的人員是否接收到這個信息,相應的工做是否已經開展等等。所以加急消息須要擴展出一個新的接口,除了基本的發送消息的功能,還須要添加監控的功能,這個時候,系統的結構如圖2所示:

輸入圖片說明

(1)先看看擴展出來的加急消息的接口,示例代碼以下:

/** 
 * 加急消息的抽象接口 
 */  
public interface UrgencyMessage extends Message{  
    /** 
     * 監控某消息的處理過程 
     * @param messageId 被監控的消息的編號 
     * @return 包含監控到的數據對象,這裏示意一下,因此用了Object 
     */  
    public Object watch(String messageId);  
}

(2)相應的實現方式仍是發送站內短消息和Email兩種,一樣須要兩個實現類來分別實現這兩種方式,先看站內短消息的方式,示例代碼以下:

public class UrgencyMessageSMS implements UrgencyMessage {  
    public void send(String message, String toUser) {  
        message = "加急:"+message;  
        System.out.println("使用站內短消息的方式,發送消息'"+message+"'給"+toUser);  
    }  
 
    public Object watch(String messageId) {  
        //獲取相應的數據,組織成監控的數據對象,而後返回         
        return null;  
    }     
} 

public class UrgencyMessageEmail implements UrgencyMessage {  
    public void send(String message, String toUser) {  
        message = "加急:"+message;  
        System.out.println("使用Email的方式,發送消息'"+message+"'給"+toUser);  
    }  
    public Object watch(String messageId) {  
        //獲取相應的數據,組織成監控的數據對象,而後返回         
        return null;  
    }     
}

(3)事實上,在實現加急消息發送的功能上,可能會使用前面發送不一樣消息的功能,也就是讓實現加急消息處理的對象繼承普通消息的相應實現,這裏爲了讓結構簡單一點,清晰一點,因此沒有這樣作。

##1.3 有何問題## 上面這樣實現,好像也能知足基本的功能要求,但是這麼實現好很差呢?有沒有什麼問題呢?

我們繼續向下來添加功能實現,爲了簡潔,就再也不去進行代碼示意了,經過實現的結構示意圖就能夠看出實現上的問題。

  1. 繼續添加特急消息的處理

特急消息不須要查看處理進程,只要沒有完成,就直接催促,也就是說,對於特急消息,在普通消息的處理基礎上,須要添加催促的功能。而特急消息、還有催促的發送方式,相應的實現方式仍是發送站內短消息和Email兩種,此時系統的結構如圖3所示:

輸入圖片說明

仔細觀察上面的系統結構示意圖,會發現一個很明顯的問題,那就是:經過這種繼承的方式來擴展消息處理,會很是不方便

你看,實現加急消息處理的時候,必須實現站內短消息和Email兩種處理方式,由於業務處理可能不一樣;在實現特急消息處理的時候,又必須實現站內短消息和Email這兩種處理方式。

這意味着,之後每次擴展一下消息處理,都必需要實現這兩種處理方式,是否是很痛苦,這還不算完,若是要添加新的實現方式呢?繼續向下看吧。

  1. 繼續添加發送手機消息的處理方式

若是看到上面的實現,你還感受問題不是很大的話,繼續完成功能,添加發送手機消息的處理方式。

仔細觀察如今的實現,若是要添加一種新的發送消息的方式,是須要在每一種抽象的具體實現裏面,都要添加發送手機消息的處理的。也就是說:發送普通消息、加急消息和特急消息的處理,均可以經過手機來發送。這就意味着,須要添加三個實現。此時系統結構如圖4所示:

輸入圖片說明

  1. 小結一下出現的問題

採用經過繼承來擴展的實現方式,有個明顯的缺點:擴展消息的種類不太容易,不一樣種類的消息具備不一樣的業務,也就是有不一樣的實現,在這種狀況下,每一個種類的消息,須要實現全部不一樣的消息發送方式。

更可怕的是,若是要新加入一種消息的發送方式,那麼會要求全部的消息種類,都要加入這種新的發送方式的實現

要是考慮業務功能上再擴展一下呢?好比:要求實現羣發消息,也就是一次能夠發送多條消息,這就意味着不少地方都得修改,太恐怖了。

那麼究竟該如何實現才能既實現功能,又能靈活的擴展呢?

#2 解決方案# ##2.1 橋接模式來解決## 用來解決上述問題的一個合理的解決方案,就是使用橋接模式。那麼什麼是橋接模式呢?

  1. 橋接模式定義

將抽象部分與它的實現部分分離,使它們均可以獨立地變化。

  1. 應用橋接模式來解決的思路

仔細分析上面的示例,根據示例的功能要求,示例的變化具備兩個緯度,一個緯度是抽象的消息這邊,包括普通消息、加急消息和特急消息,這幾個抽象的消息自己就具備必定的關係,加急消息和特急消息會擴展普通消息;另外一個緯度在具體的消息發送方式上,包括站內短消息、Email和手機短信息,這幾個方式是平等的,可被切換的方式。這兩個緯度一共能夠組合出9種不一樣的可能性來,它們的關係以下圖5所示:

輸入圖片說明

如今出現問題的根本緣由,就在於消息的抽象和實現是混雜在一塊兒的,這就致使了,一個緯度的變化,會引發另外一個緯度進行相應的變化,從而使得程序擴展起來很是困難。

要想解決這個問題,就必須把這兩個緯度分開,也就是將抽象部分和實現部分分開,讓它們相互獨立,這樣就能夠實現獨立的變化,使擴展變得簡單。

橋接模式經過引入實現的接口,把實現部分從系統中分離出去;那麼,抽象這邊如何使用具體的實現呢?確定是面向實現的接口來編程了,爲了讓抽象這邊可以很方便的與實現結合起來,把頂層的抽象接口改爲抽象類,在裏面持有一個具體的實現部分的實例

這樣一來,對於須要發送消息的客戶端而言,就只須要建立相應的消息對象,而後調用這個消息對象的方法就能夠了,這個消息對象會調用持有的真正的消息發送方式來把消息發送出去。也就是說客戶端只是想要發送消息而已,並不想關心具體如何發送。

##2.2 模式結構和說明## 輸入圖片說明

Abstraction:抽象部分的接口。一般在這個對象裏面,要維護一個實現部分的對象引用,在抽象對象裏面的方法,須要調用實現部分的對象來完成。這個對象裏面的方法,一般都是跟具體的業務相關的方法。

RefinedAbstraction:擴展抽象部分的接口,一般在這些對象裏面,定義跟實際業務相關的方法,這些方法的實現一般會使用Abstraction中定義的方法,也可能須要調用實現部分的對象來完成。

Implementor:定義實現部分的接口,這個接口不用和Abstraction裏面的方法一致,一般是由Implementor接口提供基本的操做,而Abstraction裏面定義的是基於這些基本操做的業務方法,也就是說Abstraction定義了基於這些基本操做的較高層次的操做。

ConcreteImplementor:真正實現Implementor接口的對象。

##2.3 橋接模式示例代碼##

  1. 先看看Implementor接口的定義,示例代碼以下:
/** 
 * 定義實現部分的接口,能夠與抽象部分接口的方法不同 
 */  
public interface Implementor {  
    /** 
     * 示例方法,實現抽象部分須要的某些具體功能 
     */  
    public void operationImpl();  
}
  1. 再看看Abstraction接口的定義,注意一點,雖說是接口定義,但實際上是實現成爲抽象類。示例代碼以下:
/** 
 * 定義抽象部分的接口 
 */
public abstract class Abstraction {  
    /** 
     * 持有一個實現部分的對象 
     */  
    protected Implementor impl;  
    /** 
     * 構造方法,傳入實現部分的對象  
     * @param impl 實現部分的對象 
     */  
    public Abstraction(Implementor impl){  
        this.impl = impl;  
    }  
    /** 
     * 示例操做,實現必定的功能,可能須要轉調實現部分的具體實現方法 
     */  
    public void operation() {  
        impl.operationImpl();  
    }  
}
  1. 該來看看具體的實現了,示例代碼以下:
/** 
 * 真正的具體實現對象 
 */  
public class ConcreteImplementorA implements Implementor {  
    public void operationImpl() {   
        //真正的實現  
    }  
}  

/** 
 * 真正的具體實現對象 
 */  
public class ConcreteImplementorB implements Implementor {  
    public void operationImpl() {   
        //真正的實現  
    }  
}
  1. 最後來看看擴展Abstraction接口的對象實現,示例代碼以下:
/** 
 * 擴充由Abstraction定義的接口功能 
 */  
public class RefinedAbstraction extends Abstraction {  
    public RefinedAbstraction(Implementor impl) {  
        super(impl);  
    }  
    /** 
     * 示例操做,實現必定的功能 
     */  
    public void otherOperation(){  
        //實現必定的功能,可能會使用具體實現部分的實現方法,  
        //可是本方法更大的多是使用Abstraction中定義的方法,  
        //經過組合使用Abstraction中定義的方法來完成更多的功能  
    }  
}

##2.4 使用橋接模式重寫示例## 學習了橋接模式的基礎知識事後,該來使用橋接模式重寫前面的示例了。經過示例,來看看使用橋接模式來實現一樣的功能,是否能解決「既能方便的實現功能,又能有很好的擴展性」的問題

要使用橋接模式來從新實現前面的示例,首要任務就是要把抽象部分和實現部分分離出來,分析要實現的功能,抽象部分就是各個消息的類型所對應的功能,而實現部分就是各類發送消息的方式

其次要按照橋接模式的結構,給抽象部分和實現部分分別定義接口,而後分別實現它們就能夠了。

  1. 從簡單功能開始

從相對簡單的功能開始,先實現普通消息和加急消息的功能,發送方式先實現站內短消息和Email這兩種。

使用橋接模式來實現這些功能的程序結構如圖7所示:

輸入圖片說明

(1)先看看實現部分定義的接口,示例代碼以下:

/** 
 * 實現發送消息的統一接口 
 */  
public interface MessageImplementor {  
    /** 
     * 發送消息 
     * @param message 要發送的消息內容 
     * @param toUser 消息發送的目的人員 
     */  
    public void send(String message,String toUser);  
}

(2)再看看抽象部分定義的接口,示例代碼以下:

/** 
 * 抽象的消息對象 
 */  
public abstract class AbstractMessage {  
    /** 
     * 持有一個實現部分的對象 
     */  
    protected MessageImplementor impl;  
    /** 
     * 構造方法,傳入實現部分的對象  
     * @param impl 實現部分的對象 
     */  
    public AbstractMessage(MessageImplementor impl){  
        this.impl = impl;  
    }  
    /** 
     * 發送消息,轉調實現部分的方法 
     * @param message 要發送的消息內容 
     * @param toUser 消息發送的目的人員 
     */  
    public void sendMessage(String message,String toUser){  
        this.impl.send(message, toUser);  
    }     
}

(3)看看如何具體的實現發送消息,先看站內短消息的實現吧,示例代碼以下:

/** 
 * 以站內短消息的方式發送消息 
 */  
public  class MessageSMS implements MessageImplementor{  
    public void send(String message, String toUser) {  
        System.out.println("使用站內短消息的方式,發送消息'"+message+"'給"+toUser);  
    }  
}

/** 
 * 以Email的方式發送消息 
 */  
public class MessageEmail implements MessageImplementor{  
    public void send(String message, String toUser) {  
        System.out.println("使用Email的方式,發送消息'"+message+"'給"+toUser);  
    }  
}

(4)接下來該看看如何擴展抽象的消息接口了,先看普通消息的實現,示例代碼以下:

public class CommonMessage extends AbstractMessage{  
    public CommonMessage(MessageImplementor impl) {  
        super(impl);  
    }  
    public void sendMessage(String message, String toUser) {  
        //對於普通消息,什麼都不幹,直接調父類的方法,把消息發送出去就能夠了  
        super.sendMessage(message, toUser);  
    }     
}

public class UrgencyMessage extends AbstractMessage{  
    public UrgencyMessage(MessageImplementor impl) {  
        super(impl);  
    }  
    public void sendMessage(String message, String toUser) {  
        message = "加急:"+message;  
        super.sendMessage(message, toUser);  
    }  
    /** 
     * 擴展本身的新功能:監控某消息的處理過程 
     * @param messageId 被監控的消息的編號 
     * @return 包含監控到的數據對象,這裏示意一下,因此用了Object 
     */  
    public Object watch(String messageId) {  
        //獲取相應的數據,組織成監控的數據對象,而後返回         
        return null;  
    }     
}
  1. 添加功能

看了上面的實現,發現使用橋接模式來實現也不是很困難啊,關鍵得看是否能解決前面提出的問題,那就來添加還未實現的功能看看,添加對特急消息的處理,同時添加一個使用手機發送消息的方式。該怎麼實現呢?

很簡單,只須要在抽象部分再添加一個特急消息的類,擴展抽象消息就能夠把特急消息的處理功能加入到系統中了對於添加手機發送消息的方式也很簡單,在實現部分新增長一個實現類,實現用手機發送消息的方式,也就能夠了

這麼簡單?好像看起來徹底沒有了前面所提到的問題。的確如此,採用橋接模式來實現事後,抽象部分和實現部分分離開了,能夠相互獨立的變化,而不會相互影響。所以在抽象部分添加新的消息處理,對發送消息的實現部分是沒有影響的反過來增長髮送消息的方式,對消息處理部分也是沒有影響的

(1)接着看看代碼實現,先看看新的特急消息的處理類,示例代碼以下:

public class SpecialUrgencyMessage extends AbstractMessage{  
    public SpecialUrgencyMessage(MessageImplementor impl) {  
        super(impl);  
    }  
    public void hurry(String messageId) {  
        //執行催促的業務,發出催促的信息  
    }  
    public void sendMessage(String message, String toUser) {  
        message = "特急:"+message;  
        super.sendMessage(message, toUser);  
        //還須要增長一條待催促的信息  
    }  
}

(2)再看看使用手機短消息的方式發送消息的實現,示例代碼以下:

/** 
 * 以手機短消息的方式發送消息 
 */  
public  class MessageMobile implements MessageImplementor{  
    public void send(String message, String toUser) {  
        System.out.println("使用手機短消息的方式,發送消息'"+message+"'給"+toUser);  
    }  
}
  1. 測試一下功能

看了上面的實現,可能會感受獲得,使用橋接模式來實現前面的示例事後,添加新的消息處理,或者是新的消息發送方式是如此簡單,但是這樣實現,好用嗎?寫個客戶端來測試和體會一下,示例代碼以下:

public class Client {  
    public static void main(String[] args) {  
        //建立具體的實現對象  
        MessageImplementor impl = new MessageSMS();  
        //建立一個普通消息對象  
        AbstractMessage m = new CommonMessage(impl);  
        m.sendMessage("請喝一杯茶", "小李");         
        //建立一個緊急消息對象  
        m = new UrgencyMessage(impl);  
        m.sendMessage("請喝一杯茶", "小李");         
        //建立一個特急消息對象  
        m = new SpecialUrgencyMessage(impl);  
        m.sendMessage("請喝一杯茶", "小李");  
         
        //把實現方式切換成手機短消息,而後再實現一遍  
        impl = new MessageMobile();  
        m = new CommonMessage(impl);  
        m.sendMessage("請喝一杯茶", "小李");  
        m = new UrgencyMessage(impl);  
        m.sendMessage("請喝一杯茶", "小李");  
        m = new SpecialUrgencyMessage(impl);  
        m.sendMessage("請喝一杯茶", "小李");  
    }  
}

運行結果以下:

使用站內短消息的方式,發送消息'請喝一杯茶'給小李  
使用站內短消息的方式,發送消息'加急:請喝一杯茶'給小李  
使用站內短消息的方式,發送消息'特急:請喝一杯茶'給小李  
使用手機短消息的方式,發送消息'請喝一杯茶'給小李  
使用手機短消息的方式,發送消息'加急:請喝一杯茶'給小李  
使用手機短消息的方式,發送消息'特急:請喝一杯茶'給小李

#3 模式講解# ##3.1 認識橋接模式##

  1. 什麼是橋接

在橋接模式裏面,不太好理解的就是橋接的概念,什麼是橋接?爲什麼須要橋接?如何橋接?把這些問題搞清楚了,也就基本明白橋接的含義了。

一個一個來,先看什麼是橋接?所謂橋接,通俗點說就是在不一樣的東西之間搭一個橋,讓他們可以鏈接起來,能夠相互通信和使用。那麼在橋接模式中究竟是給什麼東西來搭橋呢?就是爲被分離了的抽象部分和實現部分來搭橋,好比前面示例中抽象的消息和具體消息發送之間搭個橋。

可是這裏要注意一個問題:在橋接模式中的橋接是單向的,也就是隻能是抽象部分的對象去使用具體實現部分的對象,而不能反過來,也就是個單向橋

  1. 爲什麼須要橋接

爲了達到讓抽象部分和實現部分均可以獨立變化的目的,在橋接模式中,是把抽象部分和實現部分分離開來的,雖然從程序結構上是分開了,可是在抽象部分實現的時候,仍是須要使用具體的實現的,這可怎麼辦呢?抽象部分如何才能調用到具體實現部分的功能呢?很簡單,搭個橋不就能夠了,搭個橋,讓抽象部分經過這個橋就能夠調用到實現部分的功能了,所以須要橋接

  1. 如何橋接

這個理解上也很簡單,只要讓抽象部分擁有實現部分的接口對象,這就橋接上了,在抽象部分就能夠經過這個接口來調用具體實現部分的功能。也就是說,橋接在程序上就體現成了在抽象部分擁有實現部分的接口對象,維護橋接就是維護這個關係

  1. 獨立變化

橋接模式的意圖:使得抽象和實現能夠獨立變化,均可以分別擴充。也就是說抽象部分和實現部分是一種很是鬆散的關係,從某個角度來說,抽象部分和實現部分是能夠徹底分開的,獨立的,抽象部分不過是一個使用實現部分對外接口的程序罷了

若是這麼看橋接模式的話,就相似於策略模式了,抽象部分須要根據某個策略,來選擇真實的實現,也就是說橋接模式的抽象部分至關於策略模式的上下文。更原始的就直接相似於面向接口編程,經過接口分離的兩個部分而已。可是別忘了,橋接模式的抽象部分,是能夠繼續擴展和變化的,而策略模式只有上下文,是不存在所謂抽象部分的

那抽象和實現爲什麼還要組合在一塊兒呢?緣由是在抽象部分和實現部分仍是存在內部聯繫的,抽象部分的實現一般是須要調用實現部分的功能來實現的

  1. 動態變換功能

因爲橋接模式中的抽象部分和實現部分是徹底分離的,所以能夠在運行時動態組合具體的真實實現,從而達到動態變換功能的目的。

從另一個角度看,抽象部分和實現部分沒有固定的綁定關係了,所以同一個真實實現能夠被不一樣的抽象對象使用,反過來,同一個抽象也能夠有多個不一樣的實現。就像前面示例的那樣,好比:站內短消息的實現功能,能夠被普通消息、加急消息或是特急消息等不一樣的消息對象使用;反過來,某個消息具體的發送方式,能夠是站內短消息,或者是Email,也能夠是手機短消息等具體的發送方式。

  1. 退化的橋接模式

若是Implementor僅有一個實現,那麼就沒有必要建立Implementor接口了,這是一種橋接模式退化的狀況。這個時候Abstraction和Implementor是一對一的關係,雖然如此,也仍是要保持它們的分離狀態,這樣的話,它們纔不會相互影響,才能夠分別擴展。

也就是說,就算不要Implementor接口了,也要保持Abstraction和Implementor是分離的,模式的分離機制仍然是很是有用的。

  1. 橋接模式和繼承

繼承是擴展對象功能的一種常見手段,一般狀況下,繼承擴展的功能變化緯度都是一緯的,也就是變化的因素只有一類

對於出現變化因素有兩類的,也就是有兩個變化緯度的狀況,繼承實現就會比較痛苦。好比上面的示例,就有兩個變化緯度,一個是消息的類別,不一樣的消息類別處理不一樣;另一個是消息的發送方式。

從理論上來講,若是用繼承的方式來實現這種有兩個變化緯度的狀況,最後實際的實現類應該是兩個緯度上可變數量的乘積那麼多個。好比上面的示例,在消息類別的緯度上,目前的可變數量是3個,普通消息、加急消息和特急消息;在消息發送方式的緯度上,目前的可變數量也是3個,站內短消息、Email和手機短消息。這種狀況下,若是要實現全的話,那麼須要的實現類應該是:3 X 3 = 9個。

若是要在任何一個緯度上進行擴展,都須要實現另一個緯度上的可變數量那麼多個實現類,這也是爲什麼會感到擴展起來很困難。並且隨着程序規模的加大,會愈來愈難以擴展和維護。

橋接模式就是用來解決這種有兩個變化緯度的狀況下,如何靈活的擴展功能的一個很好的方案。其實,橋接模式主要是把繼承改爲了使用對象組合,從而把兩個緯度分開,讓每個緯度單獨去變化,最後經過對象組合的方式,把兩個緯度組合起來,每一種組合的方式就至關於原來繼承中的一種實現,這樣就有效的減小了實際實現的類的個數

從理論上來講,若是用橋接模式的方式來實現這種有兩個變化緯度的狀況,最後實際的實現類應該是兩個緯度上可變數量的和那麼多個。一樣是上面那個示例,使用橋接模式來實現,實現全的話,最後須要的實現類的數目應該是:3 + 3 = 6個。

這也從側面體現了,使用對象組合的方式比繼承要來得更靈活

  1. 橋接模式的調用順序示意圖

輸入圖片說明

##3.2 誰來橋接## 所謂誰來橋接,就是誰來負責建立抽象部分和實現部分的關係,說得更直白點,就是誰來負責建立Implementor的對象,並把它設置到抽象部分的對象裏面去,這點對於使用橋接模式來講,是十分重要的一點。

大體有以下幾種實現方式:

由客戶端負責建立Implementor的對象,並在建立抽象部分的對象的時候,把它設置到抽象部分的對象裏面去,前面的示例採用的就是這個方式

能夠在抽象部分的對象構建的時候,由抽象部分的對象本身來建立相應的Implementor的對象,固然能夠給它傳遞一些參數,它能夠根據參數來選擇並建立具體的Implementor的對象

能夠在Abstraction中選擇並建立一個缺省的Implementor的對象,而後子類能夠根據須要改變這個實現

也可使用抽象工廠或者簡單工廠來選擇並建立具體的Implementor的對象,抽象部分的類能夠經過調用工廠的方法來獲取Implementor的對象

若是使用IoC/DI容器的話,還能夠經過IoC/DI容器來建立具體的Implementor的對象,並注入回到Abstraction中

  1. 由抽象部分的對象本身來建立相應的Implementor的對象

對於這種狀況的實現,又分紅兩種,一種是須要外部傳入參數,一種是不須要外部傳入參數。

(1)從外面傳遞參數比較簡單,好比前面的示例,若是用一個type來標識具體採用哪一種發送消息的方案,而後在Abstraction的構造方法中,根據type進行建立就行了。

/** 
 * 抽象的消息對象 
 */  
public abstract class AbstractMessage {  
    /** 
     * 持有一個實現部分的對象 
     */  
    protected MessageImplementor impl;  
    /** 
     * 構造方法,傳入選擇實現部分的類型  
     * @param type 傳入選擇實現部分的類型 
     */  
    public AbstractMessage(int type){  
        if(type==1){  
            this.impl = new MessageSMS();  
        }else if(type==2){  
            this.impl = new MessageEmail();  
        }else if(type==3){  
            this.impl = new MessageMobile();  
        }  
    }     
    /** 
     * 發送消息,轉調實現部分的方法 
     * @param message 要發送的消息內容 
     * @param toUser 把消息發送的目的人員 
     */  
    public void sendMessage(String message,String toUser){  
        this.impl.send(message, toUser);  
    }     
}

(2)對於不須要外部傳入參數的狀況,那就說明是在Abstraction的實現中,根據具體的參數數據來選擇相應的Implementor對象。有可能在Abstraction的構造方法中選,也有可能在具體的方法中選。

好比前面的示例,若是發送的消息長度在100之內採用手機短消息,長度在100-1000採用站內短消息,長度在1000以上採用Email,那麼就能夠在內部方法中本身判斷實現了。

實現中,大體有以下改變:

原來protected的MessageImplementor類型的屬性,不須要了,去掉

提供一個protected的方法來獲取要使用的實現部分的對象,在這個方法裏面,根據消息的長度來選擇合適的實現對象

構造方法什麼都不用作了,也不須要傳入參數

在原來使用impl屬性的地方,要修改爲經過上面那個方法來獲取合適的實現對象了,不能直接使用impl屬性,不然會沒有值

public abstract class AbstractMessage {  
    /** 
     * 構造方法 
     */  
    public AbstractMessage() {  
        //如今什麼都不作了  
    }  
    /** 
     * 發送消息,轉調實現部分的方法 
     * @param message 要發送的消息內容 
     * @param toUser 把消息發送的目的人員 
     */  
    public void sendMessage(String message,String toUser) {        
        this.getImpl(message).send(message, toUser);  
    }  
    /** 
     * 根據消息的長度來選擇合適的實現 
     * @param message 要發送的消息 
     * @return 合適的實現對象 
     */  
    protected MessageImplementor getImpl(String message) {  
        MessageImplementor impl = null;  
        if(message == null) {  
            //若是沒有消息,默認使用站內短消息  
            impl = new MessageSMS();  
        } else if(message.length()< 100) {  
            //若是消息長度在100之內,使用手機短消息  
            impl = new MessageMobile();  
        } else if(message.length()<1000) {  
            //若是消息長度在100-1000之內,使用站內短消息  
            impl = new MessageSMS();  
        } else {  
            //若是消息長度在1000以上  
            impl = new MessageEmail();  
        }  
        return impl;  
    }  
}

(3)小結一下

對於由抽象部分的對象本身來建立相應的Implementor的對象的這種狀況,無論是否須要外部傳入參數,優勢是客戶使用簡單,並且集中控制Implementor對象的建立和切換邏輯缺點是要求Abstraction知道全部的具體的Implementor實現,並知道如何選擇和建立它們,若是從此要擴展Implementor的實現,就要求同時修改Abstraction的實現,這會很不靈活,使擴展不方便。

  1. 在Abstraction中建立缺省的Implementor對象

對於這種方式,實現比較簡單,直接在Abstraction的構造方法中,建立一個缺省的Implementor對象,而後子類根據須要,看是直接使用仍是覆蓋掉。示例代碼以下:

public abstract class AbstractMessage {  
    protected MessageImplementor impl;  
    /** 
     * 構造方法 
     */  
    public AbstractMessage(){  
        //建立一個默認的實現  
        this.impl = new MessageSMS();  
    }  
    public void sendMessage(String message,String toUser){  
        this.impl.send(message, toUser);  
    }  
}

這種方式其實還可使用工廠方法,把建立工做延遲到子類。

  1. 使用抽象工廠或者是簡單工廠

對於這種方式,根據具體的須要來選擇,若是是想要建立一系列實現對象,那就使用抽象工廠,若是是建立單個的實現對象,那就使用簡單工廠就能夠了。

直接在原來建立Implementor對象的地方,直接調用相應的抽象工廠或者是簡單工廠,來獲取相應的Implementor對象,很簡單,這個就不去示例了。

這種方法的優勢是Abstraction類不用和任何一個Implementor類直接耦合。

  1. 使用IoC/DI的方式

對於這種方式,Abstraction的實現就更簡單了,只須要實現注入Implementor對象的方法就能夠了,其它的Abstraction就無論了。

IoC/DI容器會負責建立Implementor對象,並設置回到Abstraction對象中,使用IoC/DI的方式,並不會改變Abstraction和Implementor的關係,Abstraction一樣須要持有相應的Implementor對象,一樣會把功能委託給Implementor對象去實現。

##3.3 典型例子-JDBC## 在Java應用中,對於橋接模式有一個很是典型的例子,就是:應用程序使用JDBC驅動程序進行開發的方式所謂驅動程序,指的是按照預先約定好的接口來操做計算機系統或者是外圍設備的程序

先簡單的回憶一下使用JDBC進行開發的過程,簡單的片段代碼示例以下:

String sql = "具體要操做的sql語句";  

// 1:裝載驅動  
Class.forName("驅動的名字");  

// 2:建立鏈接  
Connection conn = DriverManager.getConnection("鏈接數據庫服務的URL", "用戶名","密碼");  
  
// 3:建立statement或者是preparedStatement  
PreparedStatement pstmt = conn.prepareStatement(sql);  

// 4:執行sql,若是是查詢,再獲取ResultSet  
ResultSet rs = pstmt.executeQuery(sql);  
  
// 5:循環從ResultSet中把值取出來,封裝到數據對象中去  
while (rs.next()) {  
    // 取值示意,按名稱取值  
    String uuid = rs.getString("uuid");  
    // 取值示意,按索引取值  
    int age = rs.getInt(2);  
}  

//6:關閉  
rs.close();  
pstmt.close();  
conn.close();

從上面的示例能夠看出,咱們寫的應用程序,是面向JDBC的API在開發,這些接口就至關於橋接模式中的抽象部分的接口。那麼怎樣獲得這些API的呢?是經過DriverManager來獲得的。此時的系統結構如圖9所示:

輸入圖片說明

那麼這些JDBC的API,誰去實現呢?光有接口,沒有實現也不行啊。

該驅動程序登場了,JDBC的驅動程序實現了JDBC的API,驅動程序就至關於橋接模式中的具體實現部分。並且不一樣的數據庫,因爲數據庫實現不同,可執行的Sql也不徹底同樣,所以對於JDBC驅動的實現也是不同的,也就是不一樣的數據庫會有不一樣的驅動實現。此時驅動程序這邊的程序結構如圖10所示:

輸入圖片說明

有了抽象部分——JDBC的API,有了具體實現部分——驅動程序,那麼它們如何鏈接起來呢?就是如何橋接呢?

就是前面提到的DriverManager來把它們橋接起來,從某個側面來看,DriverManager在這裏起到了相似於簡單工廠的功能,基於JDBC的應用程序須要使用JDBC的API,如何獲得呢?就經過DriverManager來獲取相應的對象。

那麼此時系統的總體結構如圖11所示:

輸入圖片說明

經過上圖能夠看出,基於JDBC的應用程序,使用JDBC的API,至關因而對數據庫操做的抽象的擴展,算做橋接模式的抽象部分;而具體的接口實現是由驅動來完成的,驅動這邊天然就至關於橋接模式的實現部分了。而橋接的方式,再也不是讓抽象部分持有實現部分,而是採用了相似於工廠的作法,經過DriverManager來把抽象部分和實現部分對接起來,從而實現抽象部分和實現部分解耦

JDBC的這種架構,把抽象和具體分離開來,從而使得抽象和具體部分均可以獨立擴展。對於應用程序而言,只要選用不一樣的驅動,就可讓程序操做不一樣的數據庫,而無需更改應用程序,從而實如今不一樣的數據庫上移植;對於驅動程序而言,爲數據庫實現不一樣的驅動程序,並不會影響應用程序。並且,JDBC的這種架構,還合理的劃分了應用程序開發人員和驅動程序開發人員的邊界。

對於有些朋友會認爲,從局部來看,體現了策略模式,好比在上面的結構中去掉「JDBC的API和基於JDBC的應用程序」這邊,那麼剩下的部分,看起來就是一個策略模式的體現。此時的DriverManager就至關於上下文,而各個具體驅動的實現就至關因而具體的策略實現,這個理解也不算錯,可是在這裏看來,這麼理解是比較片面的。

對於這個問題,再次強調一點:對於設計模式,要從總體結構上、從本質目標上、從思想體現上來把握,而不要從局部、從表現、從特例實現上來把握。

##3.4 廣義橋接-Java中無處不橋接## 使用Java編寫程序,一個很重要的原則就是「面向接口編程」,說得準確點應該是「面向抽象編程」,因爲在Java開發中,更多的使用接口而非抽象類,所以一般就說成「面向接口編程」了

接口把具體的實現和使用接口的客戶程序分離開來,從而使得具體的實現和使用接口的客戶程序能夠分別擴展,而不會相互影響。使用接口的程序結構如圖12所示:

輸入圖片說明

可能有些朋友會以爲,聽起來怎麼像是橋接模式的功能呢?沒錯,若是把橋接模式的抽象部分先稍稍簡化一下,暫時不要RefinedAbstraction部分,那麼就跟上面的結構圖差很少了。去掉RefinedAbstraction後的簡化的橋接模式結構示意圖如圖13所示:

輸入圖片說明

是否是差很少呢?有朋友可能會以爲仍是有很大差別,差別主要在:前面接口的客戶程序是直接使用接口對象,不像橋接模式的抽象部分那樣,是持有具體實現部分的接口,這就致使畫出來的結構圖,一個是依賴,一個是聚合關聯

請思考它們的本質功能,橋接模式中的抽象部分持有具體實現部分的接口,最終目的是什麼,還不是須要經過調用具體實現部分的接口中的方法,來完成必定的功能,這跟直接使用接口沒有什麼不一樣,只是表現形式有點不同。再說,前面那個使用接口的客戶程序也能夠持有相應的接口對象,這樣從形式上就同樣了。

也就是說,從某個角度來說,橋接模式不過就是對「面向抽象編程」這個設計原則的擴展。正是經過具體實現的接口,把抽象部分和具體的實現分離開來,抽象部分至關因而使用實現部分接口的客戶程序,這樣抽象部分和實現部分就鬆散耦合了,從而能夠實現相互獨立的變化

這樣一來,幾乎能夠把全部面向抽象編寫的程序,都視做是橋接模式的體現,至少算是簡化的橋接模式,就算是廣義的橋接吧。而Java編程很強調「面向抽象編程」,所以,廣義的橋接,在Java中能夠說是無處不在。

再舉個你們最熟悉的例子來示例一下。在Java應用開發中,分層實現算是最基本的設計方式了吧,就拿你們最熟的三層架構來講,表現層、邏輯層和數據層,或許有些朋友對它們稱呼的名稱不一樣,但都是同一回事情。

三層的基本關係是表現層調用邏輯層,邏輯層調用數據層,經過什麼來進行調用呢?固然是接口了,它們的基本結構如圖14所示:

輸入圖片說明

經過接口來進行調用,使得表現層和邏輯層分離開來,也就是說表現層的變化,不會影響到邏輯層,同理邏輯層的變化不會影響到表現層。這也是同一套邏輯層和數據層,就可以同時支持不一樣的表現層實現的緣由,好比支持Swing或Web方式的表現層。

在邏輯層和數據層之間也是經過接口來調用,一樣使得邏輯層和數據層分離開,使得它們能夠獨立的擴展。尤爲是數據層,可能會有不少的實現方式,好比:數據庫實現、文件實現等,就算是數據庫實現,又有針對不一樣數據庫的實現,如Oracle、DB2等等。

總之,經過面向抽象編程,三層架構的各層都可以獨立的擴展和變化,而不會對其它層次產生影響。這正好是橋接模式的功能,實現抽象和實現的分離,從而使得它們能夠獨立的變化。固然三層架構不僅是在一個地方使用橋接模式,而是至少在兩個地方來使用了橋接模式,一個在表現層和邏輯層之間,一個在邏輯層和數據層之間。

下面先分別看看這兩個使用橋接模式的地方的程序結構,而後再綜合起來看看總體的程序結構。先看看邏輯層和數據層之間的程序結構,如圖15所示:

輸入圖片說明

再看看錶現層和邏輯層之間的結構示意,如圖16所示:

輸入圖片說明

而後再把它們結合起來,看看結合後的程序結構,如圖17所示:

輸入圖片說明

從廣義橋接模式的角度來看,平日熟悉的三層架構其實就是在組合使用橋接模式。從這個圖還能夠看出,橋接模式是能夠連續組合使用的,一個橋接模式的實現部分,能夠做爲下一個橋接模式的抽象部分。如此類推,能夠從三層架構擴展到四層、五層、直到N層架構,均可以使用橋接模式來組合。

若是從更本質的角度來看,基本上只要是面向抽象編寫的Java程序,均可以視爲是橋接模式的應用,都是讓抽象和實現相分離,從而使它們能獨立的變化。不過只要分離的目的達到了,叫不叫橋接模式就無所謂了。

##3.5 橋接模式的優缺點##

  1. 分離抽象和實現部分

橋接模式分離了抽象和實現部分,從而極大地提升了系統的靈活性。讓抽象部分和實現部分獨立開來,分別定義接口,這有助於對系統進行分層,從而產生更好的結構化的系統。對於系統的高層部分,只須要知道抽象部分和實現部分的接口就能夠了。

  1. 更好的擴展性

因爲橋接模式把抽象和實現部分分離開了,並且分別定義接口,這就使得抽象部分和實現部分能夠分別獨立的擴展,而不會相互影響,從而大大的提升了系統的可擴展性。 可動態切換實現

因爲橋接模式把抽象和實現部分分離開了,那麼在實現橋接的時候,就能夠實現動態的選擇和使用具體的實現,也就是說一個實現再也不是固定的綁定在一個抽象接口上了,能夠實現運行期間動態的切換實現。

  1. 可減小子類的個數

根據前面的講述,對於有兩個變化緯度的狀況,若是採用繼承的實現方式,大約須要兩個緯度上的可變化數量的乘積個子類;而採用橋接模式來實現的話,大約須要兩個緯度上的可變化數量的和個子類。能夠明顯地減小子類的個數

##3.6 思考橋接模式##

  1. 橋接模式的本質

橋接模式的本質:分離抽象和實現。

橋接模式最重要的工做就是分離抽象部分和實現部分,這是解決問題的關鍵。只有把抽象和實現分離開了,纔可以讓它們能夠獨立的變化;只有抽象和實現能夠獨立的變化,系統纔會有更好的可擴展性、可維護性。

至於其它的好處,好比:能夠動態切換實現、能夠減小子類個數等。都是把抽象部分和實現部分分離事後,帶來的,若是不把抽象部分和實現部分分離開,那就一切免談了。因此綜合來講,橋接模式的本質在於「分離抽象和實現」。

  1. 對設計原則的體現

(1)橋接模式很好的實現了開閉原則

一般應用橋接模式的地方,抽象部分和實現部分都是可變化的,也就是應用會有兩個變化緯度,橋接模式就是找到這兩個變化,並分別封裝起來,從而合理的實現OCP。

在使用橋接模式的時候,一般狀況下,頂層的Abstraction和Implementor是不變的,而具體的Implementor的實現,是可變的,因爲Abstraction是經過接口來操做具體的實現,所以具體的Implementor的實現是能夠擴展的,根據須要能夠有多個具體的實現。

一樣的,RefinedAbstraction也是可變的,它繼承並擴展Abstraction,一般在RefinedAbstraction的實現裏面,會調用Abstraction中的方法,經過組合使用來完成更多的功能,這些功能經常是與具體業務有關係的功能。

(2)橋接模式還很好的體現了:多用對象組合,少用對象繼承

在前面的示例中,若是使用對象繼承來擴展功能,不但讓對象之間有很強的耦合性,並且會須要不少的子類才能完成相應的功能,前面已經講述過了,須要兩個緯度上的可變化數量的乘積個子類。

而採用對象的組合,鬆散了對象之間的耦合性,不但使每一個對象變得簡單和可維護,還大大減小了子類的個數,根據前面的講述,大約須要兩個緯度上的可變化數量的和這麼多個子類。

  1. 什麼時候選用橋接模式

建議在以下狀況中,選用橋接模式:

若是你不但願在抽象和實現部分採用固定的綁定關係,能夠採用橋接模式,來把抽象和實現部分分開,而後在程序運行期間來動態的設置抽象部分須要用到的具體的實現,還能夠動態切換具體的實現。

若是出現抽象部分和實現部分都應該能夠擴展的狀況,能夠採用橋接模式,讓抽象部分和實現部分能夠獨立的變化,從而能夠靈活的進行單獨擴展,而不是攪在一塊兒,擴展一邊會影響到另外一邊。

若是但願實現部分的修改,不會對客戶產生影響,能夠採用橋接模式,客戶是面向抽象的接口在運行,實現部分的修改,能夠獨立於抽象部分,也就不會對客戶產生影響了,也能夠說對客戶是透明的。

若是採用繼承的實現方案,會致使產生不少子類,對於這種狀況,能夠考慮採用橋接模式,分析功能變化的緣由,看看是否能分離成不一樣的緯度,而後經過橋接模式來分離它們,從而減小子類的數目。

##3.7 相關模式##

  1. 橋接模式和策略模式

這兩個模式有很大的類似之處。

若是把橋接模式的抽象部分簡化來看,若是暫時不去擴展Abstraction,也就是去掉RefinedAbstraction。橋接模式簡化事後的結構圖參見圖13。再看策略模式的結構圖參見圖17.1。會發現,這個時候它們的結構都相似如圖18所示:

輸入圖片說明

經過上面的結構圖,能夠體會到橋接模式和策略模式是如此類似。能夠把策略模式的Context視作是使用接口的對象,而Strategy就是某個接口了,具體的策略實現那就至關於接口的具體實現。這樣看來的話,某些狀況下,可使用橋接模式來模擬實現策略模式的功能。

這兩個模式雖然類似,也仍是有區別的。最主要的是模式的目的不同,策略模式的目的是封裝一系列的算法,使得這些算法能夠相互替換;而橋接模式的目的是分離抽象和實現部分,使得它們能夠獨立的變化

  1. 橋接模式和狀態模式

因爲從模式結構上看,狀態模式和策略模式是同樣的,這兩個模式的關係也基本上相似於橋接模式和策略模式的關係。

只不過狀態模式的目的是封裝狀態對應的行爲,並在內部狀態改變的時候改變對象的行爲

  1. 橋接模式和模板方法模式

這兩個模式有類似之處。

雖然標準的模板方法模式是採用繼承來實現的,可是模板方法也能夠經過回調接口的方式來實現,若是把接口的實現獨立出去,那就相似於模板方法經過接口去調用具體的實現方法了。這樣的結構就和簡化的橋接模式相似了。

可使用橋接模式來模擬實現模板方法模式的功能。若是在實現Abstraction對象的時候,在裏面定義方法,方法裏面就是某個固定的算法骨架,也就是說這個方法就至關於模板方法。在模板方法模式裏,是把不能肯定實現的步驟延遲到子類去實現;如今在橋接模式裏面,把不能肯定實現的步驟委託給具體實現部分去完成,經過回調實現部分的接口,來完成算法骨架中的某些步驟。這樣一來,就能夠實現使用橋接模式來模擬實現模板方法模式的功能了。

使用橋接模式來模擬實現模板方法模式的功能,還有個潛在的好處,就是模板方法也能夠很方便的擴展和變化了。在標準的模板方法裏面,一個問題就是當模板發生變化的時候,全部的子類都要變化,很是不方便。而使用橋接模式來實現相似的功能,就沒有這個問題了。

另外,這裏只是說從實現具體的業務功能上,橋接模式能夠模擬實現出模板方法模式能實現的功能,並非說橋接模式和模板方法模式就變成同樣的,或者是橋接模式就能夠替換掉模板方法模式了。要注意它們自己的功能、目的、本質思想都是不同的。

  1. 橋接模式和抽象工廠模式

這兩個模式能夠組合使用。

橋接模式中,抽象部分須要獲取相應的實現部分的接口對象,那麼誰來建立實現部分的具體實現對象呢?這就是抽象工廠模式派上用場的地方。也就是使用抽象工廠模式來建立和配置一個特定的具體實現的對象。

事實上,抽象工廠主要是用來建立一系列對象的,若是建立的對象不多,或者是很簡單,還能夠採用簡單工廠,能夠達到同樣的效果,可是會比抽象工廠來得簡單。

  1. 橋接模式和適配器模式

這兩個模式能夠組合使用。

這兩個模式功能是徹底不同的,適配器模式的功能主要是用來幫助無關的類協同工做,重點在解決本來因爲接口不兼容而不能一塊兒工做的那些類,使得它們能夠一塊兒工做。而橋接模式則重點在分離抽象和實現部分

因此在使用上,一般在系統設計完成事後,纔會考慮使用適配器模式;而橋接模式,是在系統開始的時候就要考慮使用。

雖然功能上不同,這兩個模式仍是能夠組合使用的,好比:已有實現部分的接口,可是有些不太適應如今新的功能對接口的須要,徹底拋棄吧,有些功能還用得上,該怎麼辦呢?那就使用適配器來進行適配,使得舊的接口可以適應新的功能的須要。

相關文章
相關標籤/搜索