一、橋梁(Bridge)模式
橋梁模式是一個非常有用的模式,也是比較復雜的一個模式。熟悉這個模式對於理解面向對象的設計原則,包括"開-閉"原則(OCP)以及組合/聚合復用原則(CARP)都很有幫助。理解好這兩個原則,有助於形成正確的設計思想和培養良好的設計風格。
注:《Java與模式》一書認為Bridge模式不是一個使用頻率很高的模式,我不太贊同,我認為Bridge模式中蘊涵了很多設計模式的關鍵思想在裡面,所以我這裡采納了《Design Patterns Explained》一書的作者Alan Shalloway與James R. Trott的觀點:The Bridge pattern is quite a bit more complex than the other patterns you just learned; it is also much more useful.
橋梁模式的用意
【GOF95】在提出橋梁模式的時候指出,橋梁模式的用意是"將抽象化(Abstraction)與實現化(Implementation)脫耦,使得二者可以獨立地變化"。這句話有三個關鍵詞,也就是抽象化、實現化和脫耦。
抽象化
存在於多個實體中的共同的概念性聯系,就是抽象化。作為一個過程,抽象化就是忽略一些信息,從而把不同的實體當做同樣的實體對待【LISKOV94】。
實現化
抽象化給出的具體實現,就是實現化。
脫耦
所謂耦合,就是兩個實體的行為的某種強關聯。而將它們的強關聯去掉,就是耦合的解脫,或稱脫耦。在這裡,脫耦是指將抽象化和實現化之間的耦合解脫開,或者說是將它們之間的強關聯改換成弱關聯。
將兩個角色之間的繼承關系改為聚合關系,就是將它們之間的強關聯改換成為弱關聯。因此,橋梁模式中的所謂脫耦,就是指在一個軟件系統的抽象化和實現化之間使用組合/聚合關系而不是繼承關系,從而使兩者可以相對獨立地變化。這就是橋梁模式的用意。
二、橋梁模式的結構
橋梁模式【GOF95】是對象的結構模式,又稱為柄體(Handle and Body)模式或接口(Interface)模式。
下圖所示就是一個實現了橋梁模式的示意性系統的結構圖。
可以看出,這個系統含有兩個等級結構,也就是:
由抽象化角色和修正抽象化角色組成的抽象化等級結構。
由實現化角色和兩個具體實現化角色所組成的實現化等級結構。
橋梁模式所涉及的角色有:
抽象化(Abstraction)角色:抽象化給出的定義,並保存一個對實現化對象的引用。
修正抽象化(Refined Abstraction)角色:擴展抽象化角色,改變和修正父類對抽象化的定義。
實現化(Implementor)角色:這個角色給出實現化角色的接口,但不給出具體的實現。必須指出的是,這個接口不一定和抽象化角色的接口定義相同,實際上,這兩個接口可以非常不一樣。實現化角色應當只給出底層操作,而抽象化角色應當只給出基於底層操作的更高一層的操作。
具體實現化(Concrete Implementor)角色:這個角色給出實現化角色接口的具體實現。
三、橋梁模式的示意性源代碼
// Bridge pattern -- Structural example
using System;
// "Abstraction"
class Abstraction
{
// Fields
protected Implementor implementor;
// Properties
public Implementor Implementor
{
set{ implementor = value; }
}
// Methods
virtual public void Operation()
{
implementor.Operation();
}
}
// "Implementor"
abstract class Implementor
{
// Methods
abstract public void Operation();
}
// "RefinedAbstraction"
class RefinedAbstraction : Abstraction
{
// Methods
override public void Operation()
{
implementor.Operation();
}
}
// "ConcreteImplementorA"
class ConcreteImplementorA : Implementor
{
// Methods
override public void Operation()
{
Console.WriteLine("ConcreteImplementorA Operation");
}
}
// "ConcreteImplementorB"
class ConcreteImplementorB : Implementor
{
// Methods
override public void Operation()
{
Console.WriteLine("ConcreteImplementorB Operation");
}
}
/**//// <summary>
/// Client test
/// </summary>
public class Client
{
public static void Main( string[] args )
{
Abstraction abstraction = new RefinedAbstraction();
// Set implementation and call
abstraction.Implementor = new ConcreteImplementorA();
abstraction.Operation();
// Change implemention and call
abstraction.Implementor = new ConcreteImplementorB();
abstraction.Operation();
}
}
四、調制解調器問題
感覺《敏捷軟件開發-原則、模式與實踐》中關於Bridge模式的例子很好。(《Java與模式》一書33章的對變化的封裝一節也寫得很不錯,推薦大家讀一讀。它深入的闡述了《Design Patterns Explained》一書中"1)Design to interfaces. 2)Favor composition over inheritance. 3)Find what varies and encapsulate it"的三個觀點。)。
如圖所示,有大量的調制解調器客戶程序在使用Modem接口。Modem接口被幾個派生類HayesModem、USRoboticsModem和EarniesModem實現。它很好地遵循了OCP、LSP和DIP。當增加新種類的調制解調器時,調制解調器的客戶程序不會受影響。
假定這種情形持續了幾年,並有許多調制解調器的客戶程序都在使用著Modem接口。現出現了一種不撥號的調制解調器,被稱為專用調制解調器。它們位於一條專用連接的兩端。有幾個新應用程序使用這些專用調制解調器,它們無需撥號。我們稱這些使用者為DedUser。但是,客戶希望當前所有的調制解調器客戶程序都可以使用這些專用調制解調器。他們不希望去更改許許多多的調制解調器客戶應用程序,所以完全可以讓這些調制解調器客戶程序去撥一些假(dummy)電話號碼。
如果能選擇的話,我們會把系統的設計更改為下圖所示的那樣。
我們把撥號和通信功能分離為兩個不同的接口。原來的調制解調器實現這兩個接口,而調制解調器客戶程序使用這兩個接口。DedUser只使用Modem接口,而DedicateModem只實現Modem接口。但這樣做會要求我們更改所有的調制解調器客戶程序--這是客戶不允許的。
一個可能的解決方案是讓DedicatedModem從Modem派生並且把dial方法和hangup方法實現為空,就像下面這樣:
幾個月後,已經有了大量的DedUser,此時客戶提出了一個新的更改。為了能撥國際電話號碼、信用卡電話、PIN標識電話等等,必修對現有dial中使用char[10]存儲號碼改為能夠撥打任意長度的電話號碼。
顯然,所有的調制解調器客戶程序都必須更改。客戶同意了對調制解調器客戶程序的更改,因為他們別無選擇。糟糕的是,現在必須要去告訴DedUser的編寫者,他們必須要更改他們的代碼!你可以想象他們聽到這個會有多高興。本來他們是不用調用dial的。
這就是許多項目都會具有的那種有害的混亂依賴關系。系統某一部分中的一個雜湊體(kludge)創建了一個有害的依賴關系,最終導致系統中完全無關的部分出現問題。
如果使用ADAPTER模式解決最初的問題的話,就可以避免這個嚴重問題。如圖:
請注意,雜湊體仍然存在。適配器仍然要模擬連接狀態。然而,所有的依賴關系都是從適配器發起的。雜湊體和系統隔離,藏身於幾乎無人知曉的適配器中。
BRIDGE模式
看待這個問題,還有另外一個方式。現在,出現了另外一種切分Modem層次結構的方式。如下圖:
這不是一個理想的結構。每當增加一款新硬件時,就必須創建兩個新類--一個針對專用的情況,一個針對撥號的情況。每當增加一種新連接類型時,就必須創建3個新類,分別對應3款不同的硬件。如果這兩個自由度根本就是不穩定的,那麼不用多久,就會出現大量的派生類。
在類型層次結構具有多個自由度的情況中,BRIDGE模式通常是有用的。我們可以把這些層次結構分開並通過橋把它們結合到一起,而不是把它們合並起來。如圖:
我們把調制解調器類層次結構分成兩個層次結構。一個表示連接方法,另一個表示硬件。
這個結構雖然復雜,但是很有趣。它的創建不會影響到調制解調器的使用者,並且還完全分離了連接策略和硬件實現。ModemConnectController的每個派生類代表了一個新的連接策略。在這個策略的實現中可以使用sendlmp、receivelmp、diallmp和hanglmp。新imp方法的增加不會影響到使用者。可以使用ISP來給連接控制類增加新的接口。這種做法可以創建出一條遷移路徑,調制解調器的客戶程序可以沿著這條路徑慢慢地得到一個比dial和hangup層次更高的API。
五、另外一個實際應用Bridge模式的例子
該例子演示了業務對象(BusinessObject)通過Bridge模式與數據對象(DataObject)解耦。數據對象的實現可以在不改變客戶端代碼的情況下動態進行更換。
// Bridge pattern -- Real World example
using System;
using System.Collections;
// "Abstraction"
class BusinessObject
{
// Fields
private DataObject dataObject;
protected string group;
// Constructors
public BusinessObject( string group )
{
this.group = group;
}
// Properties
public DataObject DataObject
{
set{ dataObject = value; }
get{ return dataObject; }
}
// Methods
virtual public void Next()
{ dataObject.NextRecord(); }
virtual public void Prior()
{ dataObject.PriorRecord(); }
virtual public void New( string name )
{ dataObject.NewRecord( name ); }
virtual public void Delete( string name )
{ dataObject.DeleteRecord( name ); }
virtual public void Show()
{ dataObject.ShowRecord(); }
virtual public void ShowAll()
{
Console.WriteLine( "Customer Group: {0}", group );
dataObject.ShowAllRecords();
}
}
// "RefinedAbstraction"
class CustomersBusinessObject : BusinessObject
{
// Constructors
public CustomersBusinessObject( string group )
: base( group ){}
// Methods
override public void ShowAll()
{
// Add separator lines
Console.WriteLine();
Console.WriteLine( "------------------------" );
base.ShowAll();
Console.WriteLine( "------------------------" );
}
}
// "Implementor"
abstract class DataObject
{
// Methods
abstract public void NextRecord();
abstract public void PriorRecord();
abstract public void NewRecord( string name );
abstract public void DeleteRecord( string name );
abstract public void ShowRecord();
abstract public void ShowAllRecords();
}
// "ConcreteImplementor"
class CustomersDataObject : DataObject
{
// Fields
private ArrayList customers = new ArrayList();
private int current = 0;
// Constructors
public CustomersDataObject()
{
// Loaded from a database
customers.Add( "Jim Jones" );
customers.Add( "Samual Jackson" );
customers.Add( "Allen Good" );
customers.Add( "Ann Stills" );
customers.Add( "Lisa Giolani" );
}
// Methods
public override void NextRecord()
{
if( current <= customers.Count - 1 )
current++;
}
public override void PriorRecord()
{
if( current > 0 )
current--;
}
public override void NewRecord( string name )
{
customers.Add( name );
}
public override void DeleteRecord( string name )
{
customers.Remove( name );
}
public override void ShowRecord()
{
Console.WriteLine( customers[ current ] );
}
public override void ShowAllRecords()
{
foreach( string name in customers )
Console.WriteLine( " " + name );
}
}
/**//// <summary>
/// Client test
/// </summary>
public class BusinessApp
{
public static void Main( string[] args )
{
// Create RefinedAbstraction
CustomersBusinessObject customers =
new CustomersBusinessObject(" Chicago ");
// Set ConcreteImplementor
customers.DataObject = new CustomersDataObject();
// Exercise the bridge
customers.Show();
customers.Next();
customers.Show();
customers.Next();
customers.Show();
customers.New( "Henry Velasquez" );
customers.ShowAll();
}
}
六、在什麼情況下應當使用橋梁模式
根據上面的分析,在以下的情況下應當使用橋梁模式:
如果一個系統需要在構件的抽象化角色和具體化角色之間增加更多的靈活性,避免在兩個層次之間建立靜態的聯系。
設計要求實現化角色的任何改變不應當影響客戶端,或者說實現化角色的改變對客戶端是完全透明的。
一個構件有多於一個的抽象化角色和實現化角色,系統需要它們之間進行動態耦合。
雖然在系統中使用繼承是沒有問題的,但是由於抽象化角色和具體化角色需要獨立變化,設計要求需要獨立管理這兩者。