橋樑模式
一、引子
下面的一個例子,有助於理解Bridge模式的設計目的:
設想要繪製一幅圖畫,藍天、白雲、綠樹、小鳥,如果畫面尺寸很大,那麼用蠟筆繪製就會遇到點麻煩。畢竟細細的蠟筆要塗出一片藍天,是有些麻煩。如果有可能,最好有套大號蠟筆,粗粗的蠟筆很快能塗抹完成。至於色彩嗎,最好每種顏色來支粗的,除了藍天還有綠地呢。這樣,如果一套12種顏色的蠟筆,我們需要兩套24支,同種顏色的一粗一細。呵呵,畫還沒畫,開始做夢了:要是再有一套中號蠟筆就更好了,這樣,不多不少總共36支蠟筆。
再看看毛筆這一邊,居然如此簡陋:一套水彩12色,外加大中小三支毛筆。你可別小瞧這"簡陋"的組合,畫藍天用大毛筆,畫小鳥用小毛筆,各具特色。
呵呵,您是不是已經看出來了,不錯,我今天要說的就是Bridge模式。爲了一幅畫,我們需要準備36支型號不同的蠟筆,而改用毛筆三支就夠了,當然還要搭配上12種顏料。通過Bridge模式,我們把乘法運算3×12=36改爲了加法運算3+12=15,這一改進可不小。那麼我們這裏蠟筆和毛筆到底有什麼區別呢?
實際上,蠟筆和毛筆的關鍵一個區別就在於筆和顏色是否能夠分離。橋樑模式的用意是"將抽象化 (Abstraction)與實現化(Implementation)脫耦,使得二者可以獨立地變化"。關鍵就在於能否脫耦。蠟筆的顏色和蠟筆本身是分不開的,所以就造成必須使用36支色彩、大小各異的蠟筆來繪製圖畫。而毛筆與顏料能夠很好的脫耦,各自獨立變化,便簡化了操作。在這裏,抽象層面的概念是:"毛筆用顏料作畫",而在實現時,毛筆有大中小三號,顏料有紅綠藍等12種,於是便可出現3×12種組合。每個參與者(毛筆與顏料)都可以在自己的自由度上隨意轉換。
蠟筆由於無法將筆與顏色分離,造成筆與顏色兩個自由度無法單獨變化,使得只有創建36種對象才能完成任務。Bridge模式將繼承關係轉換爲組合關係,從而降低了系統間的耦合,減少了代碼編寫量。但這僅僅是Bridge模式帶來的衆多好處的一部分,
二、橋樑模式的定義
"將抽象化(Abstraction)與實現化(Implementation)脫耦,使得二者可以獨立地變化"。這句話有三個關鍵詞,也就是抽象化、實現化和脫耦。
抽象化
存在於多個實體中的共同的概念性聯繫,就是抽象化。作爲一個過程,抽象化就是忽略一些信息,從而把不同的實體當做同樣的實體對待。
實現化
抽象化給出的具體實現,就是實現化。
脫耦
所謂耦合,就是兩個實體的行爲的某種強關聯。而將它們的強關聯去掉,就是耦合的解脫,或稱脫耦。在這裏,脫耦是指將抽象化和實現化之間的耦合解脫開,或者說是將它們之間的強關聯改換成弱關聯。
將兩個角色之間的繼承關係改爲聚合關係,就是將它們之間的強關聯改換成爲弱關聯。因此,橋樑模式中的所謂脫耦,就是指在一個軟件系統的抽象化和實現化之間使用組合/聚合關係而不是繼承關係,從而使兩者可以相對獨立地變化。這就是橋樑模式的用意。
三、模式解析
3.1、類圖
3.2、包含的角色
可以看出,這個系統含有兩個等級結構,也就是:
·由抽象化角色和修正抽象化角色組成的抽象化等級結構。
·由實現化角色和兩個具體實現化角色所組成的實現化等級結構。
橋樑模式所涉及的角色有:
·抽象化(Abstraction)角色:抽象化給出的定義,並保存一個對實現化對象的引用。
·修正抽象化(RefinedAbstraction)角色:擴展抽象化角色,改變和修正父類對抽象化的定義。
·實現化(Implementor)角色:這個角色給出實現化角色的接口,但不給出具體的實現。必須指出的是,這個接口不一定和抽象化角色的接口定義相同,實際上,這兩個接口可以非常不一樣。實現化角色應當只給出底層操作,而抽象化角色應當只給出基於底層操作的更高一層的操作。
·具體實現化(ConcreteImplementor)角色:這個角色給出實現化角色接口的具體實現。
3.3、一般代碼
四、使用場合
-
你不希望在抽象和它的實現部分有一個固定的綁定關係。例如這種情況可能是因爲程序在運行時刻部分應用可以被選擇或者切換。
-
類的抽象以及它的實現都可以通過生成子類的方法加以擴充。
-
對一個抽象的實現部分的修改應對客戶不產生影響,即客戶端的代碼不必重新編譯。
-
你想在多個對象之間共享實現,但是同時要求客戶並不知道這一點
五、例子(不同記錄方式、不同平臺的日誌記錄工具)
在創建型模式裏面,我曾經提到過抽象與實現,抽象不應該依賴於具體實現細節,實現細節應該依賴於抽象。看下面這幅圖:
在這種情況下,如果抽象B穩定,而實現細節b變化,這時用創建型模式來解決沒有問題。但是如果抽象B也不穩定,也是變化的,該如何解決?這就要用到Bridge模式了。
我們用日誌記錄工具這個例子來說明Bridge模式。現在我們要開發一個通用的日誌記錄工具,它支持數據庫記錄DatabaseLog和文本文件記錄FileLog兩種方式,同時它既可以運行在.NET平臺,也可以運行在Java平臺上。
根據我們的設計經驗,應該把不同的日誌記錄方式分別作爲單獨的對象來對待,併爲日誌記錄類抽象出一個基類Log出來,各種不同的日誌記錄方式都繼承於該基類:
圖 Log類結構圖
實現代碼如下
Log
public interface Log
{
public void write(String log);
}
DatabaseLog
public class DatabaseLog implements Log
{
public void write(String log)
{
// ......Log Database
}
}
TextFileLog
public class TextFileLog implements Log
{
public void write(String log)
{
// ......Log Text File
}
}
另外考慮到不同平臺的日誌記錄,對於操作數據庫、寫入文本文件所調用的方式可能是不一樣的,爲此對於不同的日誌記錄方式,我們需要提供各種不同平臺上的實現,對上面的類做進一步的設計得到了下面的結構圖:
圖
實現代碼如下:
JDatabaseLog
public class JDatabaseLog extends DatabaseLog
{
@Override
public void write(String log)
{
//......(Java平臺)Log Database
}
}
JTextFileLog
public class JTextFileLog extends TextFileLog
{
@Override
public void write(String log)
{
//......(Java平臺)Log TextFile
}
}
NDatabaseLog
public class NDatabaseLog extends DatabaseLog
{
@Override
public void write(String log)
{
//......(.NET平臺)Log Database
}
}
NTextFileLog
public class NTextFileLog extends TextFileLog
{
@Override
public void write(String log)
{
//......(.NET平臺)Log Text File
}
}
現在的這種設計方案本身是沒有任何錯誤的,假如現在我們要引入一種新的xml文件的記錄方式,則上面的類結構圖會變成:
如圖中藍色的部分所示,我們新增加了一個繼承於Log基類的子類,而沒有修改其它的子類,這樣也符合了開放-封閉原則。如果我們引入一種新的平臺,比如說我們現在開發的日誌記錄工具還需要支持Borland平臺,此時該類結構又變成了:
同樣我們沒有修改任何的東西,只是增加了兩個繼承於DatabaseLog和TextFileLog的子類,這也符合了開放-封閉原則。
但是我們說這樣的設計是脆弱的,仔細分析就可以發現,它還是存在很多問題,首先它在遵循開放-封閉原則的同時,違背了類的單一職責原則,即一個類只有一個引起它變化的原因,而這裏引起Log類變化的原因卻有兩個,即日誌記錄方式的變化和日誌記錄平臺的變化;其次是重複代碼會很多,不同的日誌記錄方式在不同的平臺上也會有一部分的代碼是相同的;再次是類的結構過於複雜,繼承關係太多,難於維護,最後最致命的一點是擴展性太差。上面我們分析的變化只是沿着某一個方向,如果變化沿着日誌記錄方式和不同的運行平臺兩個方向變化,我們會看到這個類的結構會迅速的變龐大。
現在該是Bridge模式粉墨登場的時候了,我們需要解耦這兩個方向的變化,把它們之間的強耦合關係改成弱聯繫。我們把日誌記錄方式和不同平臺上的實現分別當作兩個獨立的部分來對待,對於日誌記錄方式,類結構圖仍然是:
圖 不同記錄方式
現在我們引入另外一個抽象類ImpLog,它是日誌記錄在不同平臺的實現的基類,結構圖如下:
圖 不同平臺
實現代碼如下:
ImpLog
public interface ImpLog
{
public void Execute(String msg);
}
JImpLog
public class JImpLog implements ImpLog
{
public void Execute(String msg)
{
// ...... Java平臺
}
}
NImpLog
public class NImpLog implements ImpLog
{
public void Execute(String msg)
{
//...... .NET平臺
}
}
這時對於日誌記錄方式和不同的運行平臺這兩個類都可以獨立的變化了,我們要做的工作就是把這兩部分之間連接起來。那如何連接呢?在這裏,Bridge使用了對象組合的方式,類結構圖如下:
圖
實現代碼如下:
Log
public abstract class Log
{
protected ImpLog implementor;
public void setImplementor(ImpLog implementor) {
this.implementor = implementor;
}
public abstract void write(String log);
}
TextFileLog
public class TextFileLog extends Log
{
@Override
public void write(String log)
{
implementor.Execute(log);
}
}
DatabaseLog
public class DatabaseLog extends Log
{
@Override
public void write(String log)
{
implementor.Execute(log);
}
}
Client
public class Client
{
public static void main(String[] args)
{
//.NET平臺下的Database Log
Log dblog = new DatabaseLog();
dblog.setImplementor(new NImpLog()) ;
dblog.write(".net ");
//Java平臺下的Text File Log
Log txtlog = new TextFileLog();
txtlog.setImplementor(new JImpLog());
txtlog.write("java");
}
}
原文地址 http://www.programfan.com/blog/article.asp?id=15963