延續上一篇裝飾器模式的話題,我們繼續對需求進行升級。
示例
需求
還是以奶茶店為例,但是我們不再僅僅考慮奶茶的成分了,要想奶茶賣的好,還得需要一個響亮的品牌,奶茶有很多品牌,如一點點,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;
}
}
問題
初級方案和遇到的問題也跟裝飾器模式幾乎是一模一樣的:
- 類爆炸;
- 大量程式碼重複;
- 如果增加品牌,或者調整規格價格,程式碼維護困難,嚴重違反開閉原則。
思考
到了這裡,我們不妨思考一下如下兩個問題:
- 考慮能否使用裝飾器模式實現?
- 這裡的品牌、規格跟裝飾器模式中用到的配料有什麼區別?
很明顯,通過裝飾器模式肯定是可以達成目標的,畢竟他們看起來差不多,但是這樣好不好呢?這就需要回答第二問題,品牌、規格跟配料有區別嗎?
- 首先,一杯奶茶可以同時加多種配料,但是不能同時屬於多種品牌或者多種規格,例如可以有紅豆布丁奶茶,卻不可以有一點點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
就是橋,橋接的是Drink
,BrandBase
,SkuBase
三個維度,你沒看錯,這裡的Drink
起到了兩個作用,一個作用是飲品基類,另一個作用就是橋。其實,橋的目的就是為了將多個維度聯絡起來,因此,也可以單純的通過多繼承實現,或者單純的通過組合實現。但是,高階語言一般都不支援多繼承,並且,我們也知道,繼承並不是一個好的設計方式,因此不選擇多繼承;另外,如果純粹的通過組合實現,就需要額外定義一個無意義的橋類,這個類同時將Drink
,BrandBase
,SkuBase
組合進來,雖然可行,但這明顯是不夠優雅的,因此,橋接模式依然採用的是繼承+組合的模式。
優缺點
優點
- 分離抽象部分與它的實現部分
- 相對於繼承有更少的子類,使用更靈活,可在多個維度上自由擴充套件
缺點
- 增加系統的理解與設計難度;
- 獨立變化的維度的識別比較困難;
- 客戶端使用成本較高,需要對各個維度進行構造。
跟裝飾器模式的區別
- 裝飾器模式是為了動態地給一個物件增加功能,而橋接模式是為了讓類在多個維度上自由擴充套件;
- 裝飾器模式的裝飾者和被裝飾者需要繼承自同一父類,而橋接模式通常不需要;
- 裝飾器模式通常可以巢狀使用,而橋接模式不能。
總結
有看過前一篇裝飾器模式相關介紹的朋友可能會有所疑惑,我們這裡同樣改動了Drink
基類啊,前面不是說不應該修改原有的類嗎?其實原因很簡單,因為目的不一樣了,裝飾器模式是為了動態附加職能,而橋接模式是為了可以在多個維度上自由擴充套件。說到底,橋接模式適用在設計階段,也就是在設計Drink
類的時候,目的是為了把Drink
設計的更好用,而不是動態的在原有的Drink
類上額外增加內容,正因如此,在設計之初,維度的識別也就顯得至關重要了。