Bridge模式

oneyu發表於2003-03-03

摘要:本文首先解釋了Bridge模式的定義。然後通過一個例子,一步步將Bridge模式實現。

在一切開始之前,請允許我先給出三條經典名言:Design to interfaces. Find what varies and encapsulate it. Favor composition over inheritance.後面我們會反覆,並且是反反覆覆的用到。我認為在做設計的時候這三句話要牢牢的印在腦子裡。

一. 定義

根據GOF的定義,Bridge模式的目的是解耦抽象與它的實現,以便二者可以獨立的變化。這個定義中最容易誤解的抽象它的實現。因為這兩個詞在物件導向的語言中都有對應的關鍵字。在Java中即是“abstract”“implement”,所以很容易造成困惑的是認為要解耦一個抽象類和它的實現類。實際上,這裡實現是指的抽象類和它的派生類用以實現自己的物件,進一步說就是這裡的抽象指的是一個概念或者說是一個繼承體系中的物件,而實現抽象使用並完成自己的功能。舉個例子,這是呂震宇兄想出的一個例子(http://www.cnblogs.com/zhenyulu/articles/67016.html),這是我見過的最經典的例子了,以至於我不得不在這裡重複一遍:

小時候我們都用蠟筆畫畫,一盒蠟筆12種顏色。一開始我都是用最小號的蠟筆畫個太陽公公、月亮婆婆足夠了。後來開始畫一些抽象派的作品,就得換中號的了,要不然畫個背景都要描半天,好一盒中號的也是12種顏色。再後來我開始轉向豪放派,中號就有些捉襟見肘了,只好換大號的了,好一盒大號的也只有12種顏色。你看,像我這樣不太出名的畫家就需要36種畫筆,哇,太麻煩了。但是據我觀察,另一些比我出名的畫家倒是沒有這麼多筆,他們只有幾把刷子和一些顏料,這樣就解決了蠟筆的種類爆炸問題。如下圖所示,注意圖也是從呂兄的網站偷來的。


我要用
36種蠟筆

齊白石老先生只用3種毛筆和12種顏料

回到上面的定義,定義裡面的抽象指的就是毛筆,實現就是顏料。顏料被毛筆使用,用以完成毛筆的功能。


http://www.niufish.com/books/Pattern/com/niufish/pattern/bridge/package-use.html


在下認為上圖中有以下幾點可以變通:

1. AbstractionImplementor的聚合關係。該關係可以由RefinedAbstractionImplementor的聚合關係代替。理由是Java程式設計的時候Abstraction可能會實現為interface,不能與Implementor構成聚合關係。

2. Operation方法不一定在Abstraction中做實現,理由同上,但是必須宣告。

可見在Bridge模式中有兩個繼承體系,為了方便描述我們稱左邊的為Abstraction繼承體系,右邊為Implementor繼承體系。Abstraction使用Implementor完成自己的功能。同時,該模式允許AbstractionImplementor各自獨立變化(所謂變化,我認為就是派生)。

二. 解決的問題

上面已經說了,提出毛筆的概念目的是解決蠟筆的種類爆炸問題。3×12變成了312,並且在將來毛筆型號和顏料種類可以獨立的擴充。上面的例子在說明這種設計模式的特點和優勢上很有好處,但是畢竟我們實際編碼中很少這麼幸運的碰上這麼簡單的問題。下面我用一個相對複雜的問題來重新描述這個模式,關於蠟筆和毛筆的故事的程式碼可以到上面給出的連結查詢。

三. 一個更加複雜的例子

這裡我們給出一個更為複雜的例子,並且用一種循序漸進的方式描述,逐漸加入新的功能和約束條件。設想我們要做一個編輯器(Editor),可以開啟文字檔案,但是不同的檔案要求用不同的編輯器開啟。比如“.txt”檔案用文字編輯器開啟,而“.xml”檔案用xml編輯器開啟。


程式碼如下:

Editor介面

package com.gemplus.editor;

public abstract interface Editor {

public void openFile(String path);

}

TextEditor實現

package com.gemplus.editor; 
public class TextEditor implements Editor {
    public void openFile(String path) {
        System.out.println("Open file with Text Editor. FileName: " + path);
    }
}

XMLEditor實現

package com.gemplus.editor; 
public class XMLEditor implements Editor { 
    public void openFile(String path) {
        System.out.println("Open file with XML Editor. FileName: " + path);
    }
}

Client

package com.gemplus.editor; 
public class Client {
    public static void main(String[] args) {
        Editor xmlEditor = new XMLEditor();
        xmlEditor.openFile("test.xml"); 
        Editor textEditor = new TextEditor();
        textEditor.openFile("test.txt");
    }
}

輸出

Open file with XMLEditor. FileName: test.xml

Open file with Text Editor. FileName: test.txt

到目前為止,我們什麼模式也沒用到。不過我們還是用到了一項偉大的技術多型,還有就是對介面程式設計。程式碼寫的還算優雅,只是沒有寫註釋。

接下來我們要增加一個功能,與其說是功能,不如說是一項約束。就是,我不希望客戶端了解那些檔案要由xmlEditor開啟,哪些由textEditor開啟。但是總要有人知道,對吧?這項看似直觀的約束往往被忽略,或者在潛意識中沒有意識到。為了簡單起見,我們引入一個簡單工廠EditorImpl

public class SmartEditor implements Editor {
 
    private static Editor textEditor = new TextEditor();
    private static Editor xmlEditor = new XMLEditor();
    
    public void openFile(String path) {
        if (path.endsWith(".xml")) {
            xmlEditor.openFile(path);
        } else {
            textEditor.openFile(path);
        }
    }
}

Client也要做相應修改

public class Client {
    public static void main(String[] args) {
//        Editor xmlEditor = new XMLEditor();
//        xmlEditor.openFile("FileName");
//        Editor textEditor = new TextEditor();
//        textEditor.openFile("FileName2");
        Editor editor = new SmartEditor();
        editor.openFile("test.xml");
        editor.openFile("test.txt");
    }
}

輸出結果與修改之前完全一樣,但是現在Client端已經不需要知道應該使用哪個Editor了,這其實也是對Find what vary and encapsulate it的應用。另外一點,也可以請大家注意在EditorImpl的實現中,即繼承了Editor介面又使用了組合這其實是聯合inheritancecomposition的優點。Javaworld上有一篇文章(作者Bill Venners)就是講這個技巧和使用方法的,我找了半天沒找到,以後找到再把連結添上。

先別高興太早,我們看看目前這種設計有什麼問題。SmartEditor在充滿了技巧和高階的、偉大的技術的同時,你有沒有覺得它管的東西太多了呢?一個Editor要去關心檔名的問題,不錯SmartEditor出現的目的就是要來關心檔名,然而從概念上講,仍然不是好的設計。我們來看看什麼在變化?答案是檔案型別。OK,封裝之!

我們把它稱作什麼呢?對應與Edtior,我們不妨稱之為Editable。(說實話,我這樣這個概念建立起來有一些生硬,但是你可以想像一下:如果被編輯的東西是更加複雜的呢?我所取的例子實際上是我工作中的一個例子,要編輯的東西要比這裡描述的複雜的多!)


簡單之極,以至於我覺得沒有必要給出程式碼。但是第一次寫文章,總要給大家留點好印象。

public class FileEditable implements Editable {
    Editor editor;
    String filePath;
 
    public FileEditable(Editor editor, String filePath) {
        this.editor = editor;
        this.filePath = filePath;
    }
 
    public void open() {
        editor.openFile(filePath);
    }
 
}

Client

public class Client {
    public static void main(String[] args) {
 
        Editable editableText = new FileEditable(new TextEditor(), "test.txt");
        Editable editableXML = new FileEditable(new XMLEditor(), "test.xml");
        editableText.open();
        editableXML.open();
    }
}
輸出仍然沒有變化。(前面做的其實也可以算作是重構吧,可觀察行為沒有變化)
不得不承認,現在客戶端又需要了解什麼樣的檔案用什麼編輯器開啟了。但是一個小的變化就可以避免這一點。為FileEditable增加一個不包含Editor引數的建構函式。
    public FileEditable(String filePath) {
        this.filePath = filePath;
        if (filePath.endsWith(".xml")) {
            editor = new XMLEditor();
        } else {
            editor = new TextEditor();
        }
    }
客戶端,這裡就不給出了。

現在該是增加功能的時候了。現在我們要求,使用者不但可以開啟檔案也有可能用這個Editor來編輯一段文字,即字串。我們假設,所有的Editor都可以編輯檔案和字串。我們為Editor增加一個方法:openString。前面我們說了,Bridge模式中有兩個繼承體系,兩邊可以獨立變化。現在我們有了兩個Editable,即兩種可編輯體:檔案和字串。也可以看出我們當初把Editable封裝起來多麼的英明神武啊!!

Editable的繼承體系中又增加了一員:StringEditable


雖然我前面已經說了,
AbstractionImplementor的聚合關係可以由派生類RefinedAbstractionImplementor的聚合關係代替,為了與經典模式類圖保持儘量的相似,以便大家容易理解,我們還是在StringEditableFileEditable上面加了一層:EditableImpl


好了,我們已經實現了
Bridge模式。不過這裡面還是有一點讓人不舒服的地方,就是Editor中包含的兩個方法,實際上對應了兩種Editable。也許是這個例子舉的不好。但是《設計模式 explained》中的例子和本例大同小異。

我也是剛剛開始學習設計模式,如果有什麼不對的地方請大家指出。接下來,我會繼續這個例子,增加一個更加複雜的功能:可以巢狀組合的Editor。比如對於同一個檔案我們可能希望有兩種開啟方式,以網頁為例,我們希望在一個Editor中包含兩個子Editor,分別顯式原始碼和頁面效果。這裡面會用到Composite模式。

參考資料:

1. http://www.cnblogs.com/zhenyulu/articles/67016.html

《設計模式隨筆-蠟筆與毛筆的故事》本文對於Bridge模式給了一個相當漂亮的比喻,並且給出了程式碼示例。

2. http://www.cnblogs.com/zhenyulu/articles/62720.html

《設計模式(16)-Bridge Pattern》這篇文章沒有仔細看,前面的角色定義和解釋很精闢。

3. http://www.cnblogs.com/idior/articles/97283.html

Bridge Strategy State的區別》個人認為,作者在BridgeStrategy模式的區別上面有誤解。下面有我的回覆:

我倒是不贊同樓主所說在“蠟筆與毛筆的故事”中“缺少被抽象的行為”的說法。Bridge模式中解耦的是“抽象”與“實現”。這裡的實現不一定是對“行為”的抽象,按照《Design Pattern Explained》一書中所說,這裡實現是指的抽象類和它的派生類用以實現自己的某個物件(本身是抽象與其派生類構成的繼承體系,要不怎麼變化呢)。進一步說就是這裡的抽象指的是一個概念或者說是一個繼承體系中的物件,而實現則是被抽象使用並完成自己的功能的另一個繼承體系。橋樑模式的目的就是讓這兩個繼承體系可以,獨立的變化、派生。蠟筆與毛筆的故事中毛筆(第一個繼承體系)和顏料(第二個繼承體系)可以獨立的變化,所以我認為這是個非常恰當的橋樑模式的例子。

繼續:)

我覺得區分兩個模式的方法不是從模式的實現上面看,因為一個模式的實現往往夾雜了其它的模式,比如idior給的第一個例子中就有Template Method模式。我贊成呂震宇的說法,這裡面有Bridge模式的影子,甚至我覺得不止是影子,這本身是一個Bridge模式的例子。

區分兩個模式的方法應該從解決的問題上看,也就是從context上分析。

我覺得簡單的說Strategy模式是從N變化為1N,原來有N個類但是這N個類裡面只有某個演算法的區別,我們把N個演算法提取出來就變成了1個抽象類(不要理解成Java中的abstract class,而是這個抽象類表示一個概念)和N個實現類(同理,不要理解成對前面那個抽象類的實現,而是輔助實現抽象類的某個功能的一個繼承體系)。注意這裡只有一個繼承體系。

Bridge模式是從M×N變化為MN,原來系統中有M×N個類,但是從中可以提取出N個演算法(或者輔助類)和M個主體(我想不出一個好的名次)。這樣構成了兩個繼承體系,N個演算法(顏料)構成一個繼承體系,M個主體類(毛筆的不同型號)構成一個繼承體系。兩個繼承體系可以獨立的變化。

從解決的問題上看,二者都要解決重複程式碼的問題,但是前者不強調錐把(見呂震宇的回覆)的變化,而後者強調,並且強調錐頭和所有錐把的相容。我認為這才是二者的根本區別。

這是我個人的理解,與樓主商榷。

4. http://www.niufish.com/books/Pattern/com/niufish/pattern/bridge/package-use.html

設計模式速查,很不錯

相關文章