橋接模式——結構型模式(2)


前言

 回顧上一篇對適配器模式的介紹,其主要用於對現有對象的接口的適配封裝,使其符合復用環境的接口要求,同時相對於類適配器來說,在java語言層面更適合使用對象組合的方式來實現適配器模式(主要是因為java或者.net語言不支持多繼承機制),降低系統的耦合度,增加代碼的靈活性和可維護性。其實,Favor Composition Over Inheritance原則在多個結構型模式中都有很明顯的體現,接下來我們將要講述的橋接模式便是一例。從復雜度來說,橋接模式算是23種設計模式中較為難懂和抽象的一種模式,認識和理解其本質,對我們設計人員來說將是一個很好的提高途徑,因為它涉及了很多的面向對象設計中的原則,比如“開閉原則”及“組合/聚合復用原則”等,掌握了這些核心的設計原則,勢必會對我們形成正確的設計思路和培養良好的設計風格有所幫助。

動機

在日常的系統開發中,某些對象類型的業務邏輯的復雜性,其內部具有兩個或者多個維度的變化。如何應對這種多維度的變化?如何通過面向對象的方式來封裝隔離多個維度的變化,同時又不增加額外的類設計的復雜程度呢?橋接模式給我們提供了一個良好的解決之道,接下來就讓我們來深入地學習理解這個神奇的設計模式吧!

意圖

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

《設計模式》一書將橋接模式的意圖概括地太過於精煉和抽象,對我們初學者來說,並不太好理解。在這里,我們就多來認識一下相關概念。所謂橋接,通俗地說是就將將不同的東西搭一個橋,將兩者連通起來,這樣彼此就可以相互通訊和使用呢。而在橋接模式中,我們是通過將抽象部分與實現部分間進行“搭橋“,這樣兩者就可以相互通信,發送信息呢。不過,在橋接模式中,橋接是單向的,也就是說只有抽象部分會使用具體的實現部分對象,也就是調用其中相應的實現方法,反之不成立,實現部分是不會也不應該調用抽象部分中的相應方法的,所以這個橋接只是一個單向橋接。

之所以需要橋接,是為了讓彼此獨立變化的抽象部分和實現部分之間進行聯通, 這樣雖然從程序結構上分開呢,但是通過這個”橋“,抽象部分就可以順暢地調用到實現部分的功能呢。實現橋接,在實現上很簡單,不是讓抽象部分擁有實現部分的接口對象,然后抽象部分需要的時候可以通過這個接口對象來調用具體相應的功能呢。

最后,根據橋接模式的意圖,是為了實現抽象與實現可以獨立變化,都可以相互擴充。兩者是相當松散的關系,它們之間是完全獨立和分開的,唯一關聯就是抽象部分會保留一個對實現部分的對外接口對象在需要的時候方便調用其相應的功能罷呢。

需要注意的是,我們這里說的抽象部分與實現部分是兩個變化維度的抽象接口,也就是說它們之間不是我們平常所說的抽象與實現的關系,它們都可以單獨地去變化(擁有不同的實現),通過對象組合的方式來連接抽象部分和實現部分,這一點值得大家理解清楚哦。

結構圖

image

  1. 抽象化(Abstraction)角色:抽象類的接口,並保存一個對實現化對象的引用。
  2. 修正抽象化(Refined Abstraction)角色:擴充了Abstraction定義的接口,加強或者修正了父類對抽象化的定義。
  3. 實現化(Implementor)角色:定義實現類的接口,該接口不一定要與Abstraction的接口一致,事實上這兩個接口可以完全不同。一般來說,Implementor接口僅定義提供了底層的基本操作,而Abstraction則定義了基於這些基本操作的較高層次的操作,理解這點很關鍵哦!總結一點就是,抽象化與實現化角色之間並不存在繼承與實現的關系,兩者之間只是存在一種委托的關系而已。
  4. 具體實現化(ConcreteImplementor)角色:實現了所有實現化角色所定義的接口。

代碼示例

 1:  public abstract class Implementor{
 2:      public abstract void OperationImp();
 3:  }
 4:   
 5:  public class ConcreteImplementorA extends Implementor{
 6:      public void OperationImp(){
 7:          //省略實現...
 8:      }
 9:  }
10:   
11:  public class ConcreteImplementorB extends Implementor{
12:      public void OperationImp(){
13:          //省略實現...
14:      }
15:  }
16:  public abstract class Abstraction{
17:      protected Implementor implementor;
18:      public abstract void Operation();
19:  }
20:   
21:  public class RefinedAbstraction extends Abstraction{
22:      public RefinedAbstraction(Implementor implementor){
23:          this.implementor=implementor;
24:      }
25:   
26:      public void Operation(){
27:          //...
28:          implementor.OperationImp();//調用實現化角色實現的操作
29:          //...
30:      }
31:  }
32:   
33:  public class Client{
 
34:      public static void main(String[] args){
35:          Abstraction abstraction=new RefinedAbstraction(new ConcreteImplementorA());
36:          abstraction.Operation();
37:      }
38:  }

從上述示例代碼中,我們應該可以進一步理解上文中對抽象化與實現化兩者間的關系。其實,兩者就是組合/聚合的關系,抽象化需要保存對實現化角色的引用,因為抽象化的接口實現過程中需要調用實現化提供的相應底層操作接口。相對於示例代碼來說,類Abstraction就是抽象化角色,而類Implementor就是實現化角色,兩者定義的接口規范並不相同,也沒必要相同,因為兩者所定義的接口在實現層面上就不同,從邏輯上來說,抽象化角色定義的接口應該是更高層次的接口,而實現化角色定義的接口應該是較低層次的接口。其實,我們也可以這樣理解,實現化就是我們開頭所說的變化維度,現在我們通過對象組合的方式來隔離當前維度的變化,使代碼更具靈活性和可擴展性。

現實場景

考慮這樣一個場景——信息發送。首先信息本身就有不同的種類,比如有普通信息和加急信息(即需要特殊處理的信息,比如需要對方回執或者說在原信息內容的開頭加上特定信息),但是對它們都有一個共同的接口,就是發送信息的操作,這是一個變化的維度;另外,在現實的世界中,發送信息的手段有多種,比如通過短信(SMS)或者郵件(Email)的方式,這也一個變化的維度。現在,我們有兩個不同的變化維度,一個是信息種類,一個是信息發送手段,如果我們通過傳統的繼承方式,那么設計出來的UML圖大概會是下面的樣子:

image

雖然從上圖來說,現在好像結構並不是很復雜,而且也很合理。但是大家有沒想過,如果此時再添加一種信息種類的類,通過繼承的方式來擴展的話,這個時候我們需要添加三個類,一個是新類型的信息類,另外兩個是針對這種信息類型的兩種不同發送手段即短信和郵件的方式。如果需求至此為止的話,這樣的結構設計還是可以勉強應付的,但是不幸的是,需求是不斷變化的,或許在將來的某個時間里,我們又需要添加一種全新的信息發送手段,比如說現在用的很火的微信方式,那么這個時候需要添加的新類的數目就會很多呢。大家應該可以想像出此時的類結構圖會是什么樣子,我是不想再畫出那樣”復雜“的類結構圖來展示給大家看呢,也沒必要,繼承關系太多,難於維護,最致命的一點是這樣的繼承方式擴展性太差。其實這里不光是類數目的激增問題,更要命的是,這樣的設計已經全然違反了面向對象設計的類的單一職責原則上呢,也就是一個類應該只有一個引進它變化的原因,而這里,我們發現Message類卻存在兩個變化點,一個是信息種類,一個是信息發送手段。這樣的設計無疑是脆弱的,不合理的,接下我們通過橋接模式來重新設計以上場景,對比一下,兩者的區別之處:

image

上圖是使用橋接模式針對信息發送場景設計的類結構圖,我們不再通過繼承的方式來耦合多維度的變化,而是通過對象組合的方式來降低它們之間的耦合度,讓信息種類與信息發送手段兩個維度可以自由擴展,而將它們連接在一起的方法便是通過”橋“的方式,使信息種類可以通過信息發送手段對象來調用到相應的發送方法。通過橋接的方式,類的結構圖變得十分地簡潔、清晰,而且擁有良好的可擴展性:和上文描述的一樣,不管此時增加新的信息種類還是增加新的信息發送手段,我們都無需對原有類進行修改,只需增加必須的新類即可,甚至都無需考慮新添加的類與原來類間的關系,我們只需要繼承對應的父類即可,它們已經幫我們打理好了兩個變化維度之間的連通方式。下面給出上述場景示意性的代碼片段吧,與我們上文的示例代碼結構上幾乎完全一致!

 1:  public abstract class MessageImplementor{
 2:      public abstract void Send();
 3:  }
 4:   
 5:  public class MessageSMS extends MessageImplementor{
 6:      public void Send(){
 7:          //短信發送信息的具體實現...
 8:      }
 9:  }
10:   
11:  public class MessageEmail extends MessageImplementor{
12:      public void Send(){
13:          //郵件發送信息的具體實現...
14:      }
15:  }
16:  public abstract class Message{
17:      protected MessageImplementor implementor;
18:      public abstract void SendMessage();
19:  }
20:   
21:  public class CommonMessage extends Message{
22:      public CommonMessage(MessageImplementor implementor){
23:          this.implementor=implementor;
24:      }
25:   
26:      public void SendMessage(){
27:          //...
28:          implementor.Send();//調用具體的短信發送實現操作
29:          //...
30:      }
31:  }
32:   
33:  public class UrgencyMessage extends Message{
34:      public UrgencyMessage(MessageImplementor implementor){
35:          this.implementor=implementor;
36:      }
37:   
38:      public void SendMessage(){
39:          //...
40:          implementor.Send();//調用具體的短信發送實現操作
41:          //...
42:      }
43:  }
44:   
45:  public class Client{
46:      public static void main(String[] args){
47:          Message message=new CommonMessage(new MessageSMS());
48:          message.SendMessage();
49:      }
50:  }

通過示例代碼,我們可以清楚地看到,將信息種類和信息發送手段聯系起來的方式就是通過對象組合的方式來完成,在信息種類的父類中保存一個對信息發送手段接口的引用,以便在需要時調用其相應的實現。好呢,通過這個簡單的場景相信大家也對橋接模式的應用有了較深刻的理解呢,對其舉例就在此打住吧,大家可以充分聯想各種適用於橋接模式的應用場景,深入思考,相信會對橋接模式的本質有一個更全面准確的理解。

實現要點

  1. 橋接模式使用”對象組合“的方式來解耦抽象與實現之間綁定關系,使得各自可以沿着各自的維度來擴展和變化。
  2. 抽象部分與實現部分沿着各自維度的變化指的就是實現它們對應的子類,也就是不同的實現,這樣將兩者結合的效果就可以得到多種種類(比如信息種類)的不同實現(信息發送手段)。
  3. 橋接模式之所以不使用多繼承方式,是因為繼承方案容易違背類的單一職責原則,復用性和可擴展性都不及對象組合方式。
  4. 橋接模式是為應對兩個維度的各自變化擴展問題,如果這兩個變化的維度(或者某個維度)的變化程度並不十分劇烈,使用多繼承的方式也未嘗不可。

運用效果

  1. 分離抽象和實現部分:橋接模式分離了抽象部分和實現部分,讓抽象部分和實現部分獨立開來,分別定義接口,有助於對系統進行分層,產生更好的結構化的系統。
  2. 良好的擴展性:橋接模式把抽象部分和實現部分分離開來,而且分別定義了接口,這樣兩者就可以分別獨立擴展,並互不影響,極大地提高系統的擴展性。
  3. 可動態地切換實現:由於橋接模式把抽象部分和實現部分分離開來,這樣在實現的過程中,我們就可以實現動態的選擇和使用具體的實現部分呢。因為實現部分不是固定的綁定在一個抽象接口上,可以實現運行期間動態地切換不同實現部分。
  4. 大大減小了子類的數目:針對兩個維度的變化情況,如果采用繼承的方式,需要的兩個維度上的可變化數量(即具體子類數目)的乘積數目;而采用橋接模式,需要的只是兩個維度上的可變數量(即具體子類數目)的和個數。極大地減小子類的數目。

適用性

  1. 不希望在抽象和實現部分之間有一個固定的綁定關系時,也就是可以在運行時刻可以動態地切換實現部分的不同具體實現情況。
  2. 類的抽象和它的實現都應該可以通過生成子類的方法加以擴充。 也就說可以對不同的抽象接口與實現部分進行組合,並分別對它們時行擴充。
  3. 設計要求實現化角色的任何改變都不應當影響客戶端,也就是實現化角色的改變對客戶端是透明的。
  4. 組件有多於一個抽象化角色和實現化角色,系統需要它們之間動態解耦時。

相關模式

  1. 橋接模式與策略模式:兩者之間的類結構圖比較相似,可以將策略模式的Context當作是使用實現化接口的對象(即抽象化角色),這樣Strategy就是某一個具體的實現化部分呢。從這點來說,使用橋接模式可以模擬實現策略模式的功能,但是是有一點需要注意的是,策略模式的Context不能擴展,而橋接模式的抽象化角色卻可以自由擴展。再者就是兩者的目的不一樣,策略模式是封裝一系列算法,使得這些算法可以相互替換; 而橋接模式的目的是分離抽象部分和實現部分,使得它們可以獨立地變化。
  2. 橋接模式與狀態模式:應該說兩者的結構上來說,與狀態模式與策略模式是一樣的,兩者的關系也基本上類似於橋接模式與策略模式的關系。不同的還是各自的目的,狀態模式是封裝不同狀態下對應的行為,在內部狀態改變的時候改變對象的行為。
  3. 橋接模式與抽象工廠模式:其實凡是需要創建對象的地方就需要創建型模式,而使用最多的創建型模式就是工廠方法模式和抽象工廠模式。在橋接模式的實現過程中,抽象化部分需要引用一個實現部分的具體實現對象,而這個實現對象的創建工作可以交由工廠方法或者抽象工廠模式來完成,甚至如果所需的實現化角色的種類不多,完全可以使用簡單工廠方法來勝任。
  4. 橋接模式與適配器模式:適配器模式主要用於解決原本由於接口不兼容而不能一起工作的那些類,使得它們可以一起工作,而橋接模式重點在於分離抽象部分和實現部分,使它們彼此可以沿着各自的維度自由擴展、變化。在使用時間上,適配器模式一般用於系統設計實現之后,而橋接模式一般用於系統設計實現之時。

總結

橋接模式的本質是:分離抽象和實現。只有將兩者分離,它們才能獨立地變化,也只有兩者可以相對獨立地變化時,系統才會有更好的可擴展性和可維護性。橋接模式很好地遵循了開閉原則,也較好地體現了Favor Composition Over Inheritance(優先使用對象組合/聚合原則)。大家是否也已經體會到呢?原因上文已經介紹地很明白呢。客觀地說,橋接模式比較難理解,雖然意圖只是簡單的一句話,但是里面包含的東西卻很深刻,需要我們大家反復領悟思考,才會做到真正的融會貫通。對橋接模式的介紹就到此為止吧,接下來,我們將繼續介紹下一種結構型設計模式——組合模式,敬請期待!

 

參考資料

  1. 程傑著《大話設計模式》一書
  2. 陳臣等著《研磨設計模式》一書
  3. GOF著《設計模式》一書
  4. Terrylee .Net設計模式系列文章
  5. 呂震宇老師 設計模式系列文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM