——.NET設計模式系列之九
Terrylee
,
2006
年
2
月
概述
在軟件系統中,某些類型由于自身的邏輯,它具有兩個或多個維度的變化,那么如何應對這種“多維度的變化”?如何利用面向對象的技術來使得該類型能夠輕松的沿著多個方向進行變化,而又不引入額外的復雜度?這就要使用
Bridge
模式。
意圖
將抽象部分與實現部分分離,使它們都可以獨立的變化。
[GOF
《設計模式》
]
結構圖
圖
1 Bridge
模式結構圖
生活中的例子
橋接模式將抽象部分與它的實現分離,使它們能夠獨立地變化。一個普通的開關控制的電燈、電風扇等等,都是橋接的例子。開關的目的是將設備打開或關閉。實際的開關可以是簡單的雙刀拉鏈開關,也可以是調光開關。
圖
2
使用電子開關例子的橋接對象圖
橋接模式解說
在創建型模式里面,我曾經提到過抽象與實現,抽象不應該依賴于具體實現細節,實現細節應該依賴于抽象。看下面這幅圖:
圖
3
抽象不應該依賴于實現細節
在這種情況下,如果抽象
B
穩定,而實現細節
b
變化,這時用創建型模式來解決沒有問題。但是如果抽象
B
也不穩定,也是變化的,該如何解決?這就要用到
Bridge
模式了。
我們仍然用日志記錄工具這個例子來說明
Bridge
模式。現在我們要開發一個通用的日志記錄工具,它支持數據庫記錄
DatabaseLog
和文本文件記錄
FileLog
兩種方式,同時它既可以運行在
.NET
平臺,也可以運行在
Java
平臺上。
根據我們的設計經驗,應該把不同的日志記錄方式分別作為單獨的對象來對待,并為日志記錄類抽象出一個基類
Log
出來,各種不同的日志記錄方式都繼承于該基類:
圖
4 Log
類結構圖
實現代碼如下:
public
abstract
class
Log
{
public
abstract
void
Write(
string
log);
}
public
class
DatabaseLog
:
Log
{
public
override
void
Write(
string
log)
{
//......Log Database
}
}
public
class
TextFileLog
:
Log
{
public
override
void
Write(
string
log)
{
//......Log Text File
}
}
另外考慮到不同平臺的日志記錄,對于操作數據庫、寫入文本文件所調用的方式可能是不一樣的,為此對于不同的日志記錄方式,我們需要提供各種不同平臺上的實現,對上面的類做進一步的設計得到了下面的結構圖:
圖
5
實現代碼如下:
public
class
NDatabaseLog
:
DatabaseLog
{
public
override
void
Write(
string
log)
{
//......(.NET
平臺)Log Database
}
}
public
class
JDatabaseLog
:
DatabaseLog
{
public
override
void
Write(
string
log)
{
//......(Java
平臺)Log Database
}
}
public
class
NTextFileLog
:
TextFileLog
{
public
override
void
Write(
string
log)
{
//......(.NET
平臺)Log Text File
}
}
public
class
JTextFileLog
:
TextFileLog
{
public
override
void
Write(
string
log)
{
//......(Java
平臺)Log TextFile
}
}
現在的這種設計方案本身是沒有任何錯誤的,假如現在我們要引入一種新的
xml
文件的記錄方式,則上面的類結構圖會變成:
圖
6
如圖中藍色的部分所示,我們新增加了一個繼承于
Log
基類的子類,而沒有修改其它的子類,這樣也符合了開放
-
封閉原則。如果我們引入一種新的平臺,比如說我們現在開發的日志記錄工具還需要支持
Borland
平臺,此時該類結構又變成了:
圖
7
同樣我們沒有修改任何的東西,只是增加了兩個繼承于
DatabaseLog
和
TextFileLog
的子類,這也符合了開放
-
封閉原則。
但是我們說這樣的設計是脆弱的,仔細分析就可以發現,它還是存在很多問題,首先它在遵循開放
-
封閉原則的同時,違背了類的單一職責原則,即一個類只有一個引起它變化的原因,而這里引起
Log
類變化的原因卻有兩個,即日志記錄方式的變化和日志記錄平臺的變化;其次是重復代碼會很多,不同的日志記錄方式在不同的平臺上也會有一部分的代碼是相同的;再次是類的結構過于復雜,繼承關系太多,難于維護,最后最致命的一點是擴展性太差。上面我們分析的變化只是沿著某一個方向,如果變化沿著日志記錄方式和不同的運行平臺兩個方向變化,我們會看到這個類的結構會迅速的變龐大。
現在該是
Bridge
模式粉墨登場的時候了,我們需要解耦這兩個方向的變化,把它們之間的強耦合關系改成弱聯系。我們把日志記錄方式和不同平臺上的實現分別當作兩個獨立的部分來對待,對于日志記錄方式,類結構圖仍然是:
圖
8
現在我們引入另外一個抽象類
ImpLog
,它是日志記錄在不同平臺的實現的基類,結構圖如下:
圖
9
實現代碼如下:
public
abstract
class
ImpLog
{
public
abstract
void
Execute(
string
msg);
}
public
class
NImpLog
:
ImpLog
{
public
override
void
Execute(
string
msg)
{
//...... .NET
平臺
}
}
public
class
JImpLog
:
ImpLog
{
public
override
void
Execute(
string
msg)
{
//...... Java
平臺
}
}
這時對于日志記錄方式和不同的運行平臺這兩個類都可以獨立的變化了,我們要做的工作就是把這兩部分之間連接起來。那如何連接呢?在這里,
Bridge
使用了對象組合的方式,類結構圖如下:
圖
10
實現代碼如下:
public abstract class Log
public abstract class Log
{
protected
ImpLog
implementor;
public
ImpLog
Implementor
{
set
{ implementor =
value
; }
}
public
virtual
void
Write(
string
log)
{
implementor.Execute(log);
}
}
public
class
DatabaseLog
:
Log
{
public
override
void
Write(
string
log)
{
implementor.Execute(log);
}
}
public
class
TextFileLog
:
Log
{
public
override
void
Write(
string
log)
{
implementor.Execute(log);
}
}
可以看到,通過對象組合的方式,
Bridge
模式把兩個角色之間的繼承關系改為了耦合的關系,從而使這兩者可以從容自若的各自獨立的變化,這也是
Bridge
模式的本意。再來看一下客戶端如何去使用:
class
App
{
public
static
void
Main(
string
[] args)
{
//.NET
平臺下的Database Log
Log
dblog =
new
DatabaseLog
();
dblog.Implementor =
new
NImpLog
();
dblog.Write();
//Java
平臺下的Text File Log
Log
txtlog =
new
TextFileLog
();
txtlog.Implementor =
new
JImpLog
();
txtlog.Write();
}
}
可能有人會擔心說,這樣不就又增加了客戶程序與具體日志記錄方式之間的耦合性了嗎?其實這樣的擔心是沒有必要的,因為這種耦合性是由于對象的創建所帶來的,完全可以用創建型模式去解決,就不是這里我們所討論的內容了。
最后我們再來考慮一個問題,為什么
Bridge
模式要使用對象組合的方式而不是用繼承呢?如果采用繼承的方式,則
Log
類,
ImpLog
類都為接口,類結構圖如下:
圖
11
實現代碼如下:
public
class
NDatabaseLog
:
DatabaseLog
,
IImpLog
{
//......
}
public
class
JDatabaseLog
:
DatabaseLog
,
IImpLog
{
//......
}
public
class
NTextFileLog
:
TextFileLog
,
IImpLog
{
//......
}
public
class
JTextFileLog
:
TextFileLog
,
IImpLog
{
//......
}
如上圖中藍色的部分所示,它們既具有日志記錄方式的特性,也具有接口
IimpLog
的特性,它已經違背了面向對象設計原則中類的單一職責原則,一個類應當僅有一個引起它變化的原因。所以采用
Bridge
模式往往是比采用多繼承更好的方案。說到這里,大家應該對
Bridge
模式有一些認識了吧?如果在開發中遇到有兩個方向上縱橫交錯的變化時,應該能夠想到使用
Bridge
模式,當然了,有時候雖然有兩個方向上的變化,但是在某一個方向上的變化并不是很劇烈的時候,并不一定要使用
Bridge
模式。
效果及實現要點
1
.
Bridge
模式使用“對象間的組合關系”解耦了抽象和實現之間固有的綁定關系,使得抽象和實現可以沿著各自的維度來變化。
2
.所謂抽象和實現沿著各自維度的變化,即“子類化”它們,得到各個子類之后,便可以任意它們,從而獲得不同平臺上的不同型號。
3
.
Bridge
模式有時候類似于多繼承方案,但是多繼承方案往往違背了類的單一職責原則(即一個類只有一個變化的原因),復用性比較差。
Bridge
模式是比多繼承方案更好的解決方法。
4
.
Bridge
模式的應用一般在“兩個非常強的變化維度”,有時候即使有兩個變化的維度,但是某個方向的變化維度并不劇烈——換言之兩個變化不會導致縱橫交錯的結果,并不一定要使用
Bridge
模式。
適用性
在以下的情況下應當使用橋梁模式:
1
.如果一個系統需要在構件的抽象化角色和具體化角色之間增加更多的靈活性,避免在兩個層次之間建立靜態的聯系。
2
.設計要求實現化角色的任何改變不應當影響客戶端,或者說實現化角色的改變對客戶端是完全透明的。
3
.一個構件有多于一個的抽象化角色和實現化角色,系統需要它們之間進行動態耦合。
4
.雖然在系統中使用繼承是沒有問題的,但是由于抽象化角色和具體化角色需要獨立變化,設計要求需要獨立管理這兩者。
總結
Bridge
模式是一個非常有用的模式,也非常復雜,它很好的符合了開放
-
封閉原則和優先使用對象,而不是繼承這兩個面向對象原則。
參考資料
閻宏,《
Java
與模式》,電子工業出版社
James W. Cooper
,《
C#
設計模式》,電子工業出版社
Alan Shalloway James R. Trott
,《
Design Patterns Explained
》,中國電力出版社
MSDN WebCast
《
C#
面向對象設計模式縱橫談
(8)
:
Bridge
橋接模式
(
結構型模式
)
》
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
