設計模式-橋接模式

煮詩君發表於2020-09-03

延續上一篇裝飾器模式的話題,我們繼續對需求進行升級。

示例

需求

還是以奶茶店為例,但是我們不再僅僅考慮奶茶的成分了,要想奶茶賣的好,還得需要一個響亮的品牌,奶茶有很多品牌,如一點點,COCO,喜茶等,除此之外,我們還要對奶茶的規格進行區分,如大杯、中杯、小杯等,不同品牌價格不同,不同規格價格也不同(不考慮太複雜的情況,就假設每種品牌和規格都有一個價格基數,總價直接累加)。

初級方案

乍一看,跟上一篇裝飾器模式中的需求沒什麼區別,不就是把配料換成了品牌和規格嗎?我們還是先看看繼承的方案:

再看看部分實現程式碼:

public abstract class Drink
{
    public string Name { get; set; }

    public int Price { get; set; }

    public abstract string Desc { get; }

    public abstract int Cost { get; }
}

public class Naicha : Drink
{
    public Naicha()
    {
        Name = "奶茶";
        Price = 8;
    }
    public override string Desc => this.Name;
    public override int Cost => this.Price;
}

public class CoCoNaicha:Naicha
{
    public CoCoNaicha()
    {
        Name += "[CoCo]";
        Price += 2;
    }
}

public class DaCoCoNaicha : CoCoNaicha
{
    public DaCoCoNaicha()
    {
        Name += "+大杯";
        Price += 3;
    }
}

問題

初級方案和遇到的問題也跟裝飾器模式幾乎是一模一樣的:

  • 類爆炸;
  • 大量程式碼重複;
  • 如果增加品牌,或者調整規格價格,程式碼維護困難,嚴重違反開閉原則。

思考

到了這裡,我們不妨思考一下如下兩個問題:

  1. 考慮能否使用裝飾器模式實現?
  2. 這裡的品牌、規格跟裝飾器模式中用到的配料有什麼區別?

很明顯,通過裝飾器模式肯定是可以達成目標的,畢竟他們看起來差不多,但是這樣好不好呢?這就需要回答第二問題,品牌、規格跟配料有區別嗎?

  • 首先,一杯奶茶可以同時加多種配料,但是不能同時屬於多種品牌或者多種規格,例如可以有紅豆布丁奶茶,卻不可以有一點點COCO奶茶;
  • 其次,奶茶配料可以加多份,例如多冰,多糖,雙份布丁等,但是奶茶只能屬於一種品牌、一種規格;
  • 最後,一杯奶茶可以不加配料,但是一定會屬於某一個品牌或者規格。

改進

發現了嗎?區別還是很大的。由於有了這些區別,實現方式自然也應該是有所不同的。其實,這裡沒有那麼複雜,不需要一步就跳到裝飾者模式,我們依舊用最老套的改進方式---繼承轉組合---就可以了。
改進後的類圖如下所示:

再看看部分實現程式碼,這裡直接將品牌和規格聚合到了飲品基類中,其中,品牌和規格面向抽象程式設計,我想就不用細說了:

public abstract class Drink
{
    private readonly BrandBase _brand;

    private readonly SkuBase _sku;
    public Drink(BrandBase brand, SkuBase sku)
    {
        this._brand = brand;
        this._sku = sku;
    }

    public string Name { get; set; }

    public int Price { get; set; }


    public string Desc
    {
        get
        {
            return this.Name + this._brand.BrandName + this._sku.SkuType;
        }
    }

    public int Cost
    {
        get
        {
            return this.Price + this._brand.Price + this._sku.Price;
        }
    }
}

public class Naicha : Drink
{
    public Naicha(BrandBase brand, SkuBase sku):base(brand,sku)
    {
        Name = "奶茶";
        Price = 8;
    }
}

品牌和規格的部分程式碼如下:

public abstract class SkuBase
{
    public abstract string SkuType { get; }

    public abstract int Price { get; }
}

public class Dabei : SkuBase
{
    public override string SkuType => "大杯";

    public override int Price => 3;
}

public abstract class BrandBase
{
    public abstract string BrandName { get; }

    public abstract int Price { get; }
}

public class CoCo : BrandBase
{
    public override string BrandName => "[CoCo]";

    public override int Price => 2;
}

比想象中的要簡單,一步就到位了。我們再來看看,如果要擴充套件增加瑞辛咖啡呢?

public class Kafei : Drink
{
    public Kafei(BrandBase brand, SkuBase sku) : base(brand, sku)
    {
        Name = "咖啡";
        Price = 12;
    }
}

public class Ruixin : BrandBase
{
    public override string BrandName => "[瑞辛]";

    public override int Price => 2;
}

沒錯,就是這麼簡單,直接在飲品和品牌兩個維度上增加咖啡和瑞辛就可以了,原有的程式碼不用做任何修改,一步改造直接就滿足了開閉原則。沒錯,這就是橋接模式。

簡化UML

將上述案例中的類圖簡化並抽象就可以得到橋接模式的UML類圖了:

  • Abstraction:抽象化角色,並儲存一個對實現化物件的引用。
  • RefinedAbstraction:修正抽象化角色,改變和修正父類對抽象化的定義。
  • Implementor:實現化角色,這個角色給出實現化角色的介面,但不給出具體的實現。
  • ConcreteImplementor:具體實現化角色,這個角色給出實現化角色介面的具體實現。

定義

橋接模式是將抽象部分與它的實現部分分離,使它們都可以獨立地變化。

有的人可能會說,既然是橋接模式,那麼橋在哪裡呢?其實,Abstraction就是橋,從奶茶店的例子來看,Drink就是橋,橋接的是DrinkBrandBaseSkuBase三個維度,你沒看錯,這裡的Drink起到了兩個作用,一個作用是飲品基類,另一個作用就是橋。其實,橋的目的就是為了將多個維度聯絡起來,因此,也可以單純的通過多繼承實現,或者單純的通過組合實現。但是,高階語言一般都不支援多繼承,並且,我們也知道,繼承並不是一個好的設計方式,因此不選擇多繼承;另外,如果純粹的通過組合實現,就需要額外定義一個無意義的橋類,這個類同時將DrinkBrandBaseSkuBase組合進來,雖然可行,但這明顯是不夠優雅的,因此,橋接模式依然採用的是繼承+組合的模式。

優缺點

優點

  • 分離抽象部分與它的實現部分
  • 相對於繼承有更少的子類,使用更靈活,可在多個維度上自由擴充套件

缺點

  • 增加系統的理解與設計難度;
  • 獨立變化的維度的識別比較困難;
  • 客戶端使用成本較高,需要對各個維度進行構造。

跟裝飾器模式的區別

  • 裝飾器模式是為了動態地給一個物件增加功能,而橋接模式是為了讓類在多個維度上自由擴充套件;
  • 裝飾器模式的裝飾者和被裝飾者需要繼承自同一父類,而橋接模式通常不需要;
  • 裝飾器模式通常可以巢狀使用,而橋接模式不能。

總結

有看過前一篇裝飾器模式相關介紹的朋友可能會有所疑惑,我們這裡同樣改動了Drink基類啊,前面不是說不應該修改原有的類嗎?其實原因很簡單,因為目的不一樣了,裝飾器模式是為了動態附加職能,而橋接模式是為了可以在多個維度上自由擴充套件。說到底,橋接模式適用在設計階段,也就是在設計Drink類的時候,目的是為了把Drink設計的更好用,而不是動態的在原有的Drink類上額外增加內容,正因如此,在設計之初,維度的識別也就顯得至關重要了。

原始碼連結

相關文章