.Net結構型設計模式之橋接模式(Bridge)

一、動機(Motivation)

在很多遊戲場景中,會有這樣的情況:【裝備】本身會有的自己固有的邏輯,比如槍支,會有型號的問題,同時現在很多的遊戲又在不同的介質平臺上運行和使用,這樣就使得遊戲的【裝備】具有瞭兩個變化的維度——一個變化的維度為“平臺的變化”,另一個變化的維度為“型號的變化”。如果我們要寫代碼實現這款遊戲,難道我們針對每種平臺都實現一套獨立的【裝備】嗎?復用在哪裡?如何應對這種“多維度的變化”?如何利用面向對象技術來使得【裝備】可以輕松地沿著“平臺”和“型號”兩個方向變化,而不引入額外的復雜度?

二、意圖(Intent)

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

橋模式不能隻是認為是抽象和實現的分離,它其實並不僅限於此。其實兩個都是抽象的部分,更確切的理解,應該是將一個事物中多個維度的變化分離。

三、結構(Structure)

其中imp的地方就是一個組合。Abstraction就是我們例子中的Tank,它的子類RefinedAbstraction就是T50等型號。Implementor是TankPlatformImplementation類,ConcreteImplementorA和ConcreteImplementorB分別是PCTankImplementation和MobileTankImplementation。

整個設計模式的關鍵就是組合的使用。

四、模式的組成

橋接模式的結構包括Abstraction、RefinedAbstraction、Implementor、ConcreteImplementorA和ConcreteImplementorB五個部分,其中: 
(1)、抽象化角色(Abstraction):抽象化給出的定義,並保存一個對實現化對象(Implementor)的引用。 
(2)、修正抽象化角色(Refined Abstraction):擴展抽象化角色,改變和修正父類對抽象化的定義。 
(3)、實現化角色(Implementor):這個角色給出實現化角色的接口,但不給出具體的實現。必須指出的是,這個接口不一定和抽象化角色的接口定義相同,實際上,這兩個接口可以非常不一樣。實現化角色應當隻給出底層操作,而抽象化角色應當隻給出基於底層操作的更高一層的操作。 
(4)、具體實現化角色(Concrete Implementor):這個角色給出實現化角色接口的具體實現。 
在橋接模式中,兩個類Abstraction和Implementor分別定義瞭抽象與行為類型的接口,通過調用兩接口的子類實現抽象與行為的動態組合。 

五、橋接模式的具體代碼實現

假如我們需要開發一個同時支持PC和手機端的坦克遊戲,遊戲在PC和手機上功能都一樣,都有同樣的類型,面臨同樣的功能需求變化,比如坦克可能有很多種不同的型號:T50,T75,T90……

對於其中的坦克設計,我們可能很容易設計出來一個Tank的抽象基類,然後各種不同型號的Tank繼承自該類;

這一步實現一點問題也沒有,也符合開閉原則,繼續往下看。

另外的變化原因

但是PC和手機上的圖形繪制、聲效、操作等實現完全不同……因此對於各種型號的坦克,都要提供各種不同平臺上的坦克實現:

我們一般會設計成這樣,但是這樣看很怪,這樣的設計會帶來很多問題:有很多重復代碼,類的結構過於復雜,難以維護,最致命的是引入任何新平臺,比如在TV上的Tank遊戲,都會讓整個類層級結構復雜化。我們做軟件,修改的時候,修改的越少越好,說明隔離的比較好。

public abstract class TankModel
{
    protected TankPlatformImplementation _tankImp;

    public TankModel(TankPlatformImplementation tankImp)
    {
        _tankImp = tankImp;
    }
    public abstract void Run();
}

public abstract class TankPlatformImplementation
{
    public abstract void MoveTankTo(int x, int y);
    public abstract void DrawTank();
    public abstract void Attack();
}

/// 

/// PC坦克
/// 
public class PCTankImplatation : TankPlatformImplementation
{
    string _tankModel;
    public PCTankImplatation(string tankModel)
    {
        _tankModel = tankModel;
    }
    /// 

    /// 繪制坦克
    /// 
    public override void DrawTank()
    {
        Console.WriteLine(_tankModel + "PC坦克繪制成功!");
    }
    /// 

    /// 坦克移動
    /// 
    /// x坐標
    /// y坐標
    public override void MoveTankTo(int x, int y)
    {
        Console.WriteLine(_tankModel + "PC坦克已經移動到瞭坐標(" + x + "," + y + ")處");
    }
    /// 

    /// 攻擊
    /// 
    public override void Attack()
    {
        Console.WriteLine(_tankModel + "PC坦克開始攻擊");
    }
}
/// 

/// T50型號坦克
/// 
public class T50 : TankModel
{
    public T50(TankPlatformImplementation tankImp) : base(tankImp) { }
    public override void Run()
    {
        _tankImp.DrawTank();
        _tankImp.MoveTankTo(100, 100);
        _tankImp.Attack();
    }
}

/// 

/// 客戶端調用
/// 
public class App
{
    void Main(string[] agrs)
    {
        T50 t = new T50(new PCTankImplatation("T50"));
        t.Run();
    }
}

使用瞭橋接模式後,當需求發生變化後就很容易來應對瞭,假如現在又多瞭一種T60型號的坦克,並且添加瞭一個手機平臺。隻需要添加T60型號的具體類和手機平臺具體類即可,如下:

/// 
/// 手機坦克
/// 
public class MobileTankImplatation : TankPlatformImplementation
{
    string _tankModel;
    public MobileTankImplatation(string tankModel)
    {
        _tankModel = tankModel;
    }
    /// 

    /// 繪制坦克
    /// 
    public override void DrawTank()
    {
        Console.WriteLine(_tankModel+"Mobile坦克繪制成功!");
    }
    /// 

    /// 坦克移動
    /// 
    /// x坐標
    /// y坐標
    public override void MoveTankTo(int x, int y)
    {
        Console.WriteLine(_tankModel+"Mobile坦克已經移動到瞭坐標(" + x + "," + y + ")處");
    }
    /// 

    /// 攻擊
    /// 
    public override void Attack()
    {
        Console.WriteLine(_tankModel+"Mobile坦克開始攻擊");
    }
}
/// 

/// T60型號坦克
/// 
public class T60 : TankModel
{
    public T60(TankPlatformImplementation tankImp) : base(tankImp) { }
    public override void Run()
    {
        _tankImp.DrawTank();
        _tankImp.MoveTankTo(400, 100);
        _tankImp.Attack();
    }
}

添加這兩個類後現在我們有T50型號、 T60型號 、PC平臺、手機平臺,雖然隻添加瞭兩個類,但現在有瞭四種組合,看客戶端代碼的調用:

/// 
/// 客戶端調用
/// 
public class App
{
    void Main(string[] agrs)
    {
       //T50在PC上
        T50 t50PC = new T50(new PCTankImplatation("T50"));
       t50PC.Run();
       //T50在Mobile上
        T50 t50Mobile = new T50(new MobileTankImplatation("T50"));
       t50Mobile.Run();
       //T60在PC上
        T60 t60PC = new T60(new PCTankImplatation("T60"));
       t60PC.Run();
       //T60在Mobile上
        T60 t60Mobile = new T60(new MobileTankImplatation("T60"));
       t60Mobile.Run();
    }
}

六、橋接模式的實現要點:

1.Bridge模式使用“對象間的組合關系”解耦瞭抽象和實現之間固有的綁定關系,使得抽象和實現可以沿著各自的維度來變化。 
2.所謂抽象和實現沿著各自維度的變化,即“子類化”它們,得到各個子類之後,便可以任意組合它們,從而獲得不同平臺上的不同型號。 
3.Bridge模式有時候類似於多繼承方案,但是多繼承方案往往違背瞭類的單一職責原則(即一個類隻有一個變化的原因),復用性比較差。Bridge模式是比多繼承方案更好的解決方法。 
4.Bridge模式的應用一般在“兩個非常強的變化維度”,有時候即使有兩個變化的維度,但是某個方向的變化維度並不劇烈——換言之兩個變化不會導致縱橫交錯的結果,並不一定要使用Bridge模式。 

1、橋接模式的優點:

(1)、把抽象接口與其實現解耦。 
(2)、抽象和實現可以獨立擴展,不會影響到對方。 
(3)、實現細節對客戶透明,對用於隱藏瞭具體實現細節。 

2、橋接模式的缺點:

增加瞭系統的復雜度 

3、橋接模式的使用場景:

(1)、如果一個系統需要在構件的抽象化角色和具體化角色之間添加更多的靈活性,避免在兩個層次之間建立靜態的聯系。 
(2)、設計要求實現化角色的任何改變不應當影響客戶端,或者實現化角色的改變對客戶端是完全透明的。 
(3)、需要跨越多個平臺的圖形和窗口系統上。 
(4)、 一個類存在兩個獨立變化的維度,且兩個維度都需要進行擴展。

下面是針對上面的例子,多繼承接口的一種寫法:

這樣PCT50既需要寫T50的實現,又要寫Platform的實現,它把型號和平臺的變化都引入瞭PCT50。這樣就把兩個本不該扭在一起的事務扭在瞭一起,這樣的設計更加糟糕,而且也違背瞭類的單一職責原則。

Bridge模式的應用一般在“兩個非常強的變化維度”,有時候即使有兩個變化的維度,但是某個方向的變化維度並不劇烈——換言之兩個變化不會導致縱橫交錯的結果,並不一定要使用Bridge模式。

橋模式並不同於適配器模式,適配器模式其實是一個事後諸葛亮,當發現以前的東西不適用瞭才去做一個彌補的措施。橋模式相對來說所做的改變比適配器模式早,它可以適用於有兩個甚至兩個以上維度的變化。

到此這篇關於.Net結構型設計模式之橋接模式(Bridge)的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: