在現實生活中,我們常常會用到兩種或多種型別的筆,比如毛筆和蠟筆。假設我們需要大、中、小三種型別的畫筆來繪製12中不同的顏色,如果我們使用蠟筆,需要準備3*12=36支。但如果使用毛筆的話,只需要提供3種型號的毛筆,外加12個顏料盒即可,涉及的物件個數僅為3+12=15,遠遠小於36卻能實現與36支蠟筆同樣的功能。如果需要新增一種畫筆,並且同樣需要12種顏色,那麼蠟筆需要增加12支,而毛筆卻只需要新增1支。通過分析,在蠟筆中,顏色和型號兩個不同的變化維度耦合在一起,無論對其中任何一個維度進行擴充套件,都勢必會影響另外一個維度。但在毛筆中,顏色和型號實現了分離,增加新的顏色或者型號都對另外一方沒有任何影響。在軟體系統中,有些型別由於自身的邏輯,它具有兩個或多個維度的變化。為了解決這種多維度變化,又不引入複雜度,這就要使用今天介紹的Bridge橋接模式。
橋接模式(Bridge) | 學習難度:★★★☆☆ | 使用頻率:★★★☆☆ |
一、跨平臺的影象瀏覽系統
1.1 需求介紹
M公司開發部想要開發一個跨平臺的影象瀏覽系統,要求該系統能夠顯示JPG、BMP、GIF、PNG等多種格式的檔案,並且能夠在Windows、Linux以及Unix等多個作業系統上執行。該系統首先將各種格式的檔案解析為畫素矩陣(Matrix),然後將畫素矩陣顯示在螢幕上,在不同的作業系統中可以呼叫不同的繪製函式來繪製畫素矩陣。該系統需要具備較好的擴充套件性以支援新的檔案格式和作業系統。
1.2 初始設計
M公司開發部的程式猿針對需求,立馬提出了一個初始的設計方案,其基本結構如下圖所示:
通過對這個設計方案的分析,發現存在以下兩個主要問題:
(1)由於採用了多重繼承結構,導致系統中類的個數急劇增加,系統中類的個數達到了17個。
(2)系統擴充套件麻煩,由於每一個具體類既包括影象檔案格式資訊,又包含作業系統資訊,因此無論增加新影象檔案格式還是新的作業系統,都需要增加大量的具體類。這將導致系統變得非常龐大,增加執行和維護開銷。
1.3 多維度變化
通過分析可以知道,這個系統存在兩個獨立變化的維度:影象檔案格式和作業系統,如下圖所示:
如何將各種不同型別的影象檔案解析為畫素矩陣與影象檔案格式本身相關,而如何在螢幕上繪製畫素矩陣又與作業系統相關。因為初始設計中將這兩種職責集中在一個類中,導致系統擴充套件麻煩,從類的設計角度分析,具體類BMPWindowsImage、BMPLinuxImage、BMPUnixImage等類違反了單一職責原則,因為有不止一個引起它們變化的原因,將影象檔案解析和畫素矩陣顯示這兩種完全不同的職責耦合在一起,任意一個職責發生變化都需要修改它們,因此係統擴充套件十分麻煩。
二、橋接模式簡介
2.1 模式概述
橋接模式是一種很實用的結構型模式,如果軟體系統中某個類存在兩個獨立變化的維度,通過該模式可以將這兩個維度分離出來,使兩者可以獨立擴充套件,讓系統更加符合單一職責原則。橋接模式主要使用抽象關聯取代傳統的多重繼承,將類之間的靜態繼承關係轉換為動態地物件組合關係,使得系統更加靈活,並易於擴充套件,同時有效地控制了系統中類的個數。
橋接模式的定義如下:
橋接(Bridge)模式:將抽象部分與其實現部分分離,使得他們都可以獨立地變化。它是一種物件結構型模式,又稱為介面模式。
2.2 模式結構
橋接模式的結構與其名稱一樣,存在一條連線兩個繼承等級結構的橋,橋接模式結果如下所示:
(1)Abstraction(抽象類):用於定義抽象類的介面,其中定義了一個實現了Implementor介面的物件並可以維護該物件,它與Implementor之間具有關聯關係,它既可以包含抽象業務方法, 也可以包含具體業務方法。
(2)RefinedAbstratction(擴充抽象類):擴充由Abstraction定義的介面,通常情況下他不再是抽象類而是具體類,實現了在Abstraction中宣告的抽象業務方法,在RefinedAbstraction中可以呼叫在Implementor中定義的業務方法。
(3)Implementor(實現類介面):定義實現類的介面,一般而言,它不與Abstraction的介面一致。它只提供基本操作,而Abstraction定義的介面可能會做更多更復雜的操作。
(4)ConcreteImplementor(具體實現類):具體實現Implementor介面,在不同的ConcreteImplementor中提供基本操作的不同實現,在程式執行時,ConcreteImplentor將替換其父類物件,提供給抽象類具體的業務操作方法。
2.3 使用橋接模式
要使用橋接模式,首先應該識別出一個類所具有的兩個獨立變化的維度,將他們設計成兩個獨立的繼承等級結構,為兩個維度都提供抽象層,並建立抽象耦合。
這裡我們看一個例子,最開始我們提到毛筆,對於它而言,型號是其固有的維度,因此可以設計一個抽象的毛壁壘,在該類中宣告並部分實現毛筆的業務方法,而將各種型號的毛筆作為其子類;顏色是毛筆的另一個維度,由於它與毛筆之間存在一種“設定”的關係,因此可以提供一個抽象的顏色介面,而將具體的顏色作為實現該介面的子類。在此,型號可以認為是毛筆的抽象部分,而顏色是毛筆的實現部分,其結構示意圖如下:
三、重構跨平臺影象瀏覽系統
3.1 重構設計
為了減少所需生成的子類數目,實現將作業系統和影象檔案格式兩個維度的分離,使他們可以獨立改變,M公司開發人員決定使用橋接模式來重構設計方案,其基本示意圖如下所示:
3.2 程式碼實現
(0)輔助類
public class Matrix { // 此處程式碼省略 }
(1)抽象類
/// <summary> /// 抽象影象類:抽象類 /// </summary> public abstract class Image { protected ImageImplementor imageImpl; public void SetImageImplementor (ImageImplementor imageImpl) { this.imageImpl = imageImpl; } public abstract void ParstFile(string fileName); }
(2)擴充抽象類
public class JPGImage : Image { public override void ParstFile(string fileName) { // 模擬解析JPG檔案並獲得一個畫素矩陣物件m Matrix m = new Matrix(); imageImpl.DoPaint(m); Console.WriteLine("{0} : 格式為JPG", fileName); } } public class BMPImage : Image { public override void ParstFile(string fileName) { // 模擬解析BMP檔案並獲得一個畫素矩陣物件m Matrix m = new Matrix(); imageImpl.DoPaint(m); Console.WriteLine("{0} : 格式為BMP", fileName); } } public class GIFImage : Image { public override void ParstFile(string fileName) { // 模擬解析GIF檔案並獲得一個畫素矩陣物件m Matrix m = new Matrix(); imageImpl.DoPaint(m); Console.WriteLine("{0} : 格式為GIF", fileName); } }
(3)實現類介面
/// <summary> /// 抽象作業系統實現類:實現類介面 /// </summary> public interface ImageImplementor { // 顯示畫素矩陣 void DoPaint(Matrix m); }
(4)具體實現類
public class WindowsImplementor : ImageImplementor { public void DoPaint(Matrix m) { // 呼叫Windows的繪製函式繪製畫素矩陣 Console.WriteLine("在Windows系統中顯示影象"); } } public class LinuxImplementor : ImageImplementor { public void DoPaint(Matrix m) { // 呼叫Linux的繪製函式繪製畫素矩陣 Console.WriteLine("在Linux系統中顯示影象"); } } public class UnixImplementor : ImageImplementor { public void DoPaint(Matrix m) { // 呼叫Unix的繪製函式繪製畫素矩陣 Console.WriteLine("在Unix系統中顯示影象"); } }
(5)客戶端測試
public class Program { public static void Main(string[] args) { Image image = (Image) AppConfigHelper.GetImageInstance(); ImageImplementor imageImpl = (ImageImplementor)AppConfigHelper.GetEnvInstance(); image.SetImageImplementor(imageImpl); image.ParstFile("小龍女"); Console.ReadKey(); } }
這裡為了讓系統具有更好的靈活性和可擴充套件性,引入了以下配置檔案,將具體擴充抽象類和具體實現類類名都存在了配置檔案中,再通過AppConfigHelper類進行反射生成物件。其中,配置檔案定義如下:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="RefinedAbstraction" value="Manulife.ChengDu.DesignPattern.Bridge.JPGImage, Manulife.ChengDu.DesignPattern.Bridge" /> <add key="ConcreteImplementor" value="Manulife.ChengDu.DesignPattern.Bridge.LinuxImplementor, Manulife.ChengDu.DesignPattern.Bridge" /> </appSettings> </configuration>
用於讀取配置檔案並反射生成物件的AppConfigHelper類的程式碼如下:
public class AppConfigHelper { public static string GetImageFormatName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["RefinedAbstraction"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetImageInstance() { string assemblyName = AppConfigHelper.GetImageFormatName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } public static string GetEnvName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["ConcreteImplementor"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetEnvInstance() { string assemblyName = AppConfigHelper.GetEnvName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } }
編譯後執行,輸出結果如下:
由於配置檔案設定的作業系統是Linux,圖片格式是JPG,所以輸出上圖。
這時,如果我們將配置檔案改為Windows和GIF,會輸出下圖所示:
四、橋接模式小結
4.1 主要優點
(1)分離抽象介面及其實現部分 -> 橋接模式使用“物件間的關聯關係”解耦了抽象和實現之間固有的繫結關係,使得抽象和實現可以沿著各自的維度變化
(2)取代多層繼承方案 -> 極大地減少了子類的個數
(3)提高了系統可擴充套件性 -> 在兩個變化維度中任意擴充套件一個維度,都不需要修改原有系統,符合開閉原則
4.2 主要缺點
(1)增加了系統的理解和設計難度 -> 需要開發者在一開始就對抽象層進行設計與程式設計
(2)要求正確識別出系統中兩個獨立變化的維度 -> 如何正確地識別需要一定的經驗積累
4.3 應用場景
(1)一個類存在兩個(或者多個)獨立變化的維度,而且這兩個(或者多個)維度都需要獨立進行擴充套件。
(2)不希望使用繼承或因為多層繼承而導致系統中類的個數急劇增加。
(3)一個系統需要在抽象類和具體類之間增加更多的靈活性,避免在兩個層次之間建立靜態繼承關係,通過橋接可以使它們在抽象層建立一個關聯關係。
參考資料
劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》