一起學習設計模式--08.橋接模式

模式目標

處理多維度變化

前言

現實生活中我們經常會遇到兩種類型的筆,他們分別是毛筆和蠟筆。假設需要使用大、中、小3種型號的畫筆來繪製12種不同的顏色。如果使用蠟筆,需要3 X 12 = 36 支。但是如果是毛筆的話,就不一樣了,我們只需要3種型號的毛筆,和12盒顏料即可,涉及的對象個數僅爲 3 + 12 = 15,要遠遠小於36,但是卻可以實現與36種蠟筆一樣的效果。如果要增加一種新型號的畫筆,並且也需要12種顏色,相應的蠟筆需要增加12支,但是毛筆只需要增加一支即可。通過分析得知:在蠟筆中,顏色和型號兩個不同的變化維度耦合在一起,無論是對顏色進行擴展,還是對型號進行擴展,都會對另一種維度產生影響。但在毛筆中,顏色和型號進行了分離,增加新的顏色或型號對另一方都沒有任何影響。如果使用軟件工程中的術語,可以認爲,在蠟筆中顏色和型號之間存在較強的耦合性,而毛筆很好的將二者解耦,使用起來非常靈活,擴展也更爲方便。在軟件開發中,也提供了一種設計模式來處理與畫筆類似的具有多變化維度的情況,即接下來要學習的橋接模式。

一、跨平臺圖像瀏覽系統

1.需求

A 公司打算開發一個跨平臺圖像瀏覽系統,要求該系統能夠顯示 BMP、JPG、GIF、PNG 等多種格式的文件,並且能夠在 WIndows、Linux、UNIX 等多個操作系統上運行。該系統首先將各種格式的文件解析成像素矩陣(Matrix),然後將像素矩陣顯示在屏幕上,在不同的操作系統中可以通過調用不同的繪製函數來繪製像素矩陣。該系統需具有較好的擴展性以支持新的文件格式和操作系統。

2.初始設計

A 公司開發人員針對上述要求,提出了一個初始設計方案,其基本結構如圖:

初始設計方案中,使用了多層繼承結構。但對現在的設計方案進行分析,發現存在以下兩個主要問題:

  1. 由於採用了多層繼承結構,導致系統中的類的個數急劇增加。在該設計方案中,具體層類的個數 = 所支持的圖像文件格式數 X 所支持的操作系統數。上圖中類的個數已經達到了17個。
  2. 系統擴展麻煩。由於每一個具體類既包含圖像文件格式信息,又包含了操作系統信息,因此無論是增加新的圖像文件格式還是增加新的操作系統,都需要增加大量的具體類。

3.多維度變化

通過分析可以得知,該系統存在兩個獨立變化的維度:圖像文件格式操作系統。如圖:

如何將各種不同類型的圖像文件解析爲像素矩陣與圖像文件格式本身相關,而如何在屏幕上顯示像素矩陣則僅與操作系統相關。正因爲初始設計所示的結構將這兩種職責集中在一個類中,導致系統擴展麻煩。從類的設計角度分析,具體類 BMPWindowsImage、BMPLinuxImage 和 BMPUnixImage 等違反了單一職責原則,因爲有不止一個引起它們變化的原因,它們將圖像文件解析和像素矩陣顯示這兩種完全不同的職責耦合在一起,任意一個職責發生改變都需要修改它們,因此係統擴展困難。

二、橋接模式的概述

1.定義

橋接模式是一種很實用的結構型設計模式。如果軟件系統中某個類存在兩個獨立變化的維度,通過該模式可以將這兩個維度分離出來,使兩者可以獨立擴展,讓系統更加符合單一職責原則。與多層繼承方案不同,它將兩個獨立變化的維度設計爲兩個獨立的繼承等級結構,並且在抽象層建立一個抽象關聯,該關聯關係類似一條連接兩個獨立繼承結構的橋,故名橋接模式。
橋接模式採用抽象關聯取代了傳統的多層繼承,將類之間的靜態繼承關係轉換爲動態的對象組合關係,使得系統更加靈活,並易於擴展,同時有效控制了系統中類的個數。橋接模式的定義如下:

橋接模式(Bridge Pattern):將抽象部分與其實現部分分離,使它們都可以獨立的變化。它是一種對象結構型模式,又稱接口(Interface)模式。

2.結構

橋接模式的結構與其名稱一樣,存在一條連接兩個獨立繼承等級結構的橋。橋接模式的結構如圖:

上圖包含4個角色:

  1. Abstraction(抽象類):用於定義抽象類的接口,它一般是抽象類而不是接口,其中定義了一個 Implementor (實現類接口)類型的對象並可以維護該對象。抽象類與 Implementor 之間具有關聯關係,它既可以包含抽象業務方法,也可以包含具體業務方法。
  2. RefinedAbstraction(擴充抽象類):擴充由 Abstraction 定義的接口,通常情況下它不再是抽象類而是具體類。擴充抽象類實現了在 Abstraction 中聲明的抽象業務方法,在 RefinedAbstraction 中可以調用在 Implementor 中定義的業務方法。
  3. Implementor(實現類接口):定義實現類的接口,這個接口不一定要與 Abstraction 的接口完全一致,事實上這兩個接口可以完全不同。一般而言,Implementor 接口僅提供基本操作,而 Abstraction 定義的接口可能會做更多、更復雜的操作。Implementor 接口對這些基本操作進行了聲明,而具體實現交給其子類。通過關聯關係,在 Abstraction 中不僅擁有自己的方法,還可以調用到 Implementor 中定義的方法,使用關聯關係來代替繼承。
  4. ConcreteImplementor(具體實現類):具體實現 Implementor 接口,在不同的 ConcreteImplementor 中提供基本操作的不同實現。在程序運行時,ConcreteImplementor 對象將替換其父類對象,提供給抽象類具體的業務操作方法。

3.使用

在使用橋接模式的時候,首先應該識別出一個類所具有的兩個獨立變化的維度,將它們設計爲兩個獨立的繼承等級結構,爲兩個維度都提供抽象層,並建立抽象耦合。通常情況下,將具有兩個獨立變化維度的類的一些普通業務方法和與之關係最密切的維度設計爲抽象類層次結構(抽象部分),而將另一個維度設計爲實現類層次結構(實現部分)。

例如,對於毛筆而言,由於型號是其固有的維度,因此可以設計一個抽象的毛筆類,在該類中聲明並部分實現毛筆的業務方法,而將各種型號的毛筆作爲其子類。顏色是毛筆的另一個維度,由於它與毛筆之間存在一種”設置“的關係,因此可以提供一個抽象的顏色接口,而將具體的顏色作爲實現該接口的子類。在此,型號可以認爲是毛筆的抽象部分,而顏色是毛筆的實現部分。結構示意圖如下:

三、完整解決方案

1.重構設計

爲了減少所需生成的子類數目,實現將操作系統和圖像文件格式兩個維度分離,使它們可以獨立改變,A公司開發人員使用橋接模式來重構跨平臺圖像瀏覽系統的設計。基本結構如下:

2.代碼實現

Image 充當抽象類,其子類 JPGImage、PBGImage、BMPImage、GIFImage充當擴充抽象類;ImageImp 充當實現類接口,其子類 WIndowsImp、UnixImp、LinuxImp 充當具體實現類。完整代碼如下:

    /// <summary>
    /// 像素矩陣類:輔助類
    /// 各種格式的文件最終都被轉化成像素矩陣,不同的操作系統提供不同的方式顯示像素矩陣
    /// </summary>
    public class Matrix
    {
        //此處代碼省略
    }    

    /// <summary>
    /// 抽象圖像類:抽象類
    /// </summary>
    public abstract class Image
    {
        protected IImageImp imageImp;

        public void SetImageImp(IImageImp imp)
        {
            imageImp = imp;
        }

        public abstract void ParseFile(string fileName);
    }

    /// <summary>
    /// JPG 格式圖像:擴充抽象類
    /// </summary>
    public class JPGImage : Image
    {
        public override void ParseFile(string fileName)
        {
            //模擬器解析 JPG 文件並獲得一份像素矩陣對象 m
            Matrix m = new Matrix();
            imageImp.DoPaint(m);
            Console.WriteLine(fileName + ",格式爲JPG");
        }
    }
    
    /// <summary>
    /// PNG 格式圖像:擴充抽象類
    /// </summary>
    public class PNGImage : Image
    {
        public override void ParseFile(string fileName)
        {
            //模擬器解析 PNG 文件並獲得一份像素矩陣對象 m
            Matrix m = new Matrix();
            imageImp.DoPaint(m);
            Console.WriteLine(fileName + ",格式爲PNG");
        }
    }
    
    /// <summary>
    /// BMP 格式圖像:擴充抽象類
    /// </summary>
    public class BMPImage : Image
    {
        public override void ParseFile(string fileName)
        {
            //模擬器解析 BMP 文件並獲得一份像素矩陣對象 m
            Matrix m = new Matrix();
            imageImp.DoPaint(m);
            Console.WriteLine(fileName + ",格式爲BMP");
        }
    }
    
    /// <summary>
    /// GIF 格式圖像:擴充抽象類
    /// </summary>
    public class GIFImage : Image
    {
        public override void ParseFile(string fileName)
        {
            //模擬器解析 GIF 文件並獲得一份像素矩陣對象 m
            Matrix m = new Matrix();
            imageImp.DoPaint(m);
            Console.WriteLine(fileName + ",格式爲 GIF");
        }
    }
    
    /// <summary>
    /// 抽象操作系統實現類:實現類接口
    /// </summary>
    public interface IImageImp
    {
        /// <summary>
        /// 顯示像素矩陣
        /// </summary>
        void DoPaint(Matrix matrix);
    }

    /// <summary>
    /// Windows操作系統實現類:具體實現類
    /// </summary>
    public class WindowsImp : IImageImp
    {
        public void DoPaint(Matrix matrix)
        {
            //調用Windows系統的繪製函數繪製像素矩陣
            Console.WriteLine("在 Windows 操作系統中顯示圖像");
        }
    }
    
    /// <summary>
    /// Linux操作系統實現類:具體實現類
    /// </summary>
    public class LinuxImp : IImageImp
    {
        public void DoPaint(Matrix matrix)
        {
            //調用Linux系統的繪製函數繪製像素矩陣
            Console.WriteLine("在 Linux 操作系統中顯示圖像");
        }
    }
    
    /// <summary>
    /// Unix操作系統實現類:具體實現類
    /// </summary>
    public class UnixImp : IImageImp
    {
        public void DoPaint(Matrix matrix)
        {
            //調用Unix系統的繪製函數繪製像素矩陣
            Console.WriteLine("在 Unix 操作系統中顯示圖像");
        }
    }

添加配置文件,將具體擴充抽象類和具體實現類類名都配置在配置文件中,再通過反射生成對象。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="RefinedAbstraction" value="LXP.DesignPattern.Bridge.abstractions.JPGImage"/>
    <add key="CpncreteImplementor" value="LXP.DesignPattern.Bridge.imps.WindowsImp"/>
  </appSettings>
</configuration>

配置文件幫助類

    public class AppConfigHelper
    {
        public static object GetInstance(string key)
        {
            try
            {
                var className = ConfigurationManager.AppSettings[key];
                var type = Type.GetType(className);

                return type == null ? null : Activator.CreateInstance(type);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            return null;
        }
    }

客戶端測試代碼:

    class Program
    {
        static void Main(string[] args)
        {
            var image = (Image)AppConfigHelper.GetInstance("RefinedAbstraction");
            var imp = (IImageImp)AppConfigHelper.GetInstance("CpncreteImplementor");
            image.SetImageImp(imp);
            image.ParseFile("小龍女");
        }
    }

編譯並運行,輸出結果如下:

如果以後要更換文件格式或更換操作系統,只需要修改配置文件即可。在實際使用時,可以通過分析圖像文件格式擴展名來確定具體的文件格式,在程序運行時獲取操作系統信息來確定操作系統類型,無需使用配置文件。當增加新的圖像文件格式或操作系統時,原有系統無需做任何修改,只需要增加一個對應的擴充抽象類或具體的實現類即可,系統具有較好的可擴展性,完全符合開閉原則。

四、橋接模式總結

在軟件開發中,如果一個類或一個系統有多個變化維度時,都可以嘗試使用橋接模式對其進行設計。

1.主要優點

  1. 分離抽象接口和及其實現部分。橋接模式使用“對象間的關聯關係”解耦了抽象和實現之間固有的綁定關係,使得抽象和實現可以沿着各自的維度來變化。
  2. 很多情況下,橋接模式可以取代多層集成方案。多層繼承方案違背了單一職責原則,複用性較差,且類的個數非常多。橋接模式是比多層繼承方案更好的解決方法,它極大的減少了子類的個數。
  3. 橋接模式提高了系統的可擴展性。在兩個變化維度中任意擴展一個維度,都不需要修改原有系統,符合開閉原則。

2.主要缺點

  1. 橋接模式的使用會增加系統的理解與設計難度。由於關聯關係建立在抽象層,要求開發者一開始就針對抽象層進行設計與編程。
  2. 橋接模式要求正確識別出系統中兩個獨立變化的維度,因此使用範圍具有一定的侷限性,如何正確識別兩個獨立維度也需要一定的經驗積累。

3.適用場景

  1. 如果一個系統需要在抽象類和具體類之間增加更多的靈活性,避免在兩個層次之間建立靜態的繼承關係,通過橋接模式可以使它們在抽象層建立一個關聯關係
  2. 抽象部分和實現部分可以以繼承的方式獨立擴展而互不影響,在程序運行時可以動態的將一個抽象類子類的對象和一個實現類子類的對象進行組合,即系統需要對抽象類角色和實現類角色進行動態耦合。
  3. 一個類存在兩個(或多個)獨立變化的維度,且這兩個(或多個)維度都需要獨立進行擴展。
  4. 對於那些不希望使用繼承或因爲多層繼承導致系統類的個數急劇增加的系統,橋接模式尤爲適用。

如果您覺得這篇文章有幫助到你,歡迎推薦,也歡迎關注我的公衆號。

示例代碼:

https://github.com/crazyliuxp/DesignPattern.Simples.CSharp

參考資料:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章