天天看點

.NET設計模式-橋接模式(Bridge Pattern)

——.NET設計模式系列之九

Terrylee,2006年2月

概述

在軟體系統中,某些類型由于自身的邏輯,它具有兩個或多個次元的變化,那麼如何應對這種“多元度的變化”?如何利用面向對象的技術來使得該類型能夠輕松的沿着多個方向進行變化,而又不引入額外的複雜度?這就要使用Bridge模式。

意圖

将抽象部分與實作部分分離,使它們都可以獨立的變化。[GOF 《設計模式》]

結構圖

.NET設計模式-橋接模式(Bridge Pattern)

圖1 Bridge模式結構圖

生活中的例子

橋接模式将抽象部分與它的實作分離,使它們能夠獨立地變化。一個普通的開關控制的電燈、電風扇等等,都是橋接的例子。開關的目的是将裝置打開或關閉。實際的開關可以是簡單的雙刀拉鍊開關,也可以是調光開關。

.NET設計模式-橋接模式(Bridge Pattern)

圖2 使用電子開關例子的橋接對象圖

橋接模式解說

在建立型模式裡面,我曾經提到過抽象與實作,抽象不應該依賴于具體實作細節,實作細節應該依賴于抽象。看下面這幅圖:

.NET設計模式-橋接模式(Bridge Pattern)

圖3  抽象不應該依賴于實作細節

在這種情況下,如果抽象B穩定,而實作細節b變化,這時用建立型模式來解決沒有問題。但是如果抽象B也不穩定,也是變化的,該如何解決?這就要用到Bridge模式了。

我們仍然用日志記錄工具這個例子來說明Bridge模式。現在我們要開發一個通用的日志記錄工具,它支援資料庫記錄DatabaseLog和文本檔案記錄FileLog兩種方式,同時它既可以運作在.NET平台,也可以運作在Java平台上。

根據我們的設計經驗,應該把不同的日志記錄方式分别作為單獨的對象來對待,并為日志記錄類抽象出一個基類Log出來,各種不同的日志記錄方式都繼承于該基類:

.NET設計模式-橋接模式(Bridge Pattern)

圖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

       //......Log Text File

另外考慮到不同平台的日志記錄,對于操作資料庫、寫入文本檔案所調用的方式可能是不一樣的,為此對于不同的日志記錄方式,我們需要提供各種不同平台上的實作,對上面的類做進一步的設計得到了下面的結構圖:

.NET設計模式-橋接模式(Bridge Pattern)

圖5

public class NDatabaseLog : DatabaseLog

        //......(.NET平台)Log Database

public class JDatabaseLog : DatabaseLog

        //......(Java平台)Log Database

public class NTextFileLog : TextFileLog

        //......(.NET平台)Log Text File

public class JTextFileLog : TextFileLog

        //......(Java平台)Log TextFile

現在的這種設計方案本身是沒有任何錯誤的,假如現在我們要引入一種新的xml檔案的記錄方式,則上面的類結構圖會變成:

.NET設計模式-橋接模式(Bridge Pattern)

圖6

如圖中藍色的部分所示,我們新增加了一個繼承于Log基類的子類,而沒有修改其它的子類,這樣也符合了開放-封閉原則。如果我們引入一種新的平台,比如說我們現在開發的日志記錄工具還需要支援Borland平台,此時該類結構又變成了:

.NET設計模式-橋接模式(Bridge Pattern)

圖7

同樣我們沒有修改任何的東西,隻是增加了兩個繼承于DatabaseLog和TextFileLog的子類,這也符合了開放-封閉原則。

但是我們說這樣的設計是脆弱的,仔細分析就可以發現,它還是存在很多問題,首先它在遵循開放-封閉原則的同時,違背了類的單一職責原則,即一個類隻有一個引起它變化的原因,而這裡引起Log類變化的原因卻有兩個,即日志記錄方式的變化和日志記錄平台的變化;其次是重複代碼會很多,不同的日志記錄方式在不同的平台上也會有一部分的代碼是相同的;再次是類的結構過于複雜,繼承關系太多,難于維護,最後最緻命的一點是擴充性太差。上面我們分析的變化隻是沿着某一個方向,如果變化沿着日志記錄方式和不同的運作平台兩個方向變化,我們會看到這個類的結構會迅速的變龐大。

現在該是Bridge模式粉墨登場的時候了,我們需要解耦這兩個方向的變化,把它們之間的強耦合關系改成弱聯系。我們把日志記錄方式和不同平台上的實作分别當作兩個獨立的部分來對待,對于日志記錄方式,類結構圖仍然是:

.NET設計模式-橋接模式(Bridge Pattern)

圖8

現在我們引入另外一個抽象類ImpLog,它是日志記錄在不同平台的實作的基類,結構圖如下:

.NET設計模式-橋接模式(Bridge Pattern)

圖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

        //...... Java平台

這時對于日志記錄方式和不同的運作平台這兩個類都可以獨立的變化了,我們要做的工作就是把這兩部分之間連接配接起來。那如何連接配接呢?在這裡,Bridge使用了對象組合的方式,類結構圖如下:

.NET設計模式-橋接模式(Bridge Pattern)

圖 10

    protected ImpLog implementor;

    public ImpLog Implementor

        set { implementor = value; }   

    public virtual 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類都為接口,類結構圖如下:

.NET設計模式-橋接模式(Bridge Pattern)

圖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橋接模式(結構型模式)》

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。