一起學習設計模式--10.裝飾模式

模式目標

擴展系統功能

前言

儘管目前房價依然在漲,但依舊阻止不了大家對新房的渴望和買房的熱情。如果大家買的是毛坯房,還要面臨一個艱鉅的任務,那就是裝修。對於新房進行裝修,並沒有改變房屋用於居住的本質,但是它可以讓房子變得更漂亮、更溫馨、更實用、更能滿足居家需求。在軟件設計中,也有一種類似新房裝修的技術可以對已有對象(新房)的功能進行擴展(裝修),以獲得更加符合用戶需求的對象,使得對象具有更加強大的功能。這種技術對應於一種被稱之爲裝飾模式的設計模式。

一、圖形界面構件庫的設計

A公司基於面向對象技術開發了一套圖形界面構件庫,該構件庫提供了大量基本構件,如窗體、文本框、列表框等。由於在使用該構件庫時,用戶經常要求定製一些特殊的顯示效果,如帶滾動條的窗體、帶黑色邊框的文本框、既帶滾動條又帶黑色邊框的列表框等,因此經常需要對該構件庫進行擴展以增強功能。如何提高圖形界面構件庫的可擴展性並降低其維護成本是A公司開發人員必須面對的一個問題。

開發人員針對上述要求,提出了一個基於繼承複用的初始設計方案,基本結構如下:

上圖中,在抽象類Component中聲明瞭抽象方法 Display(),其子類 Window、TextBox、ListBox等實現了 Display() 方法,可以顯示最簡單的控件,再通過它們的子類來對功能進行擴展。比如Window下邊的 SrollBarWindow、BlackBorderWindow中對Window中的Display()方法進行擴展,分別實現了帶滾動條和帶黑色邊框的窗體。但是仔細分析發現還是存在以下幾個問題:

  1. 系統擴展麻煩,在某些編程語言中無法實現。在C#、Java等面向對象的編程語言中,都不支持多重類繼承,因此在這些語言中無法通過繼承來實現對來自多個父類的方法重用。
  2. 代碼重複。上圖中可以看出,不僅是窗體需要設置滾動條,文本框、列表框等都需要設置,因此在SrollBarWindow、SrollBarTextBox、SrollBarListBox等類中都需要包含用於增加滾動條的SetScrollBar()方法。該方法的具體實現過程基本相同,代碼重複,不利於對系統進行修改和維護。
  3. 系統龐大,類的數目非常多。如果增加新的控件或者新的擴展功能,系統都需要增加大量的具體類,這將導致系統變得非常龐大。

顯然這不是一個好的設計方案,根本原因在於複用機制的不合理。如何讓系統中的類可以進行擴展但是又不會導致類的數目急劇增加呢?根據合成複用原則,在實現功能複用時,要多用關聯,少用繼承。接下來就是裝飾模式大展身手的時候了,裝飾模式,顧名思義,它的作用就是對原有對象進行裝飾,通過裝飾來擴展原有對象的功能,大大簡化系統設計。

二、裝飾模式概述

1.定義

裝飾模式可以在不改變一個對象本身功能的基礎上給對象增加額外的新行爲。裝飾模式是一種用於替代繼承的技術,它通過一種無須定義子類的方式來給對象動態增加職責,使用對象之間的關聯關係取代類之間的繼承關係。在裝飾模式中引入了裝飾類,在裝飾類中既可以調用待裝飾的原有類的方法,還可以增加新的方法,以擴充原有類的功能。
裝飾模式的定義如下:

裝飾模式(Decorator Pattern):動態地給一個對象增加一些額外的職責,就增加對象的功能來說,裝飾模式比生成子類實現更爲靈活.裝飾模式是一種對象結構型模式.

2.結構

在裝飾模式中,爲了讓系統具有更好的靈活性和可擴展性,通常會定義一個抽象裝飾類,而將具體的裝飾類作爲它的子類。裝飾模式的結構如圖:

上圖中包含4種角色:

  1. Component(抽象構件):它是具體構件和抽象裝飾類的共同父類,聲明瞭在具體構件中實現的業務方法。它的引入可以使客戶端以一致的方式處理未被裝飾的對象以及裝飾之後的對象,實現客戶端的透明操作。
  2. ConcreteComponent(具體構件):它是抽象構件類的子類,用於定義具體的構件對象,實現了在抽象構件中聲明的方法,裝飾器可以給它增加額外的職責(方法)。
  3. Decorator(抽象裝飾類):它是抽象構件類的子類,用於給具體構件增加職責,但是具體職責在其子類中實現。它維護一個指向抽象構件對象的引用,通過該引用可以調用裝飾之前構件對象的方法,並通過其子類擴展該方法,以達到裝飾的目的。
  4. ConcreteDecorate(具體裝飾類):它是抽象裝飾類的子類,負責向構件添加新的職責。每一個具體的裝飾類都定義了一些新的行爲,可以調用在抽象裝飾類中定義的方法,並可以增加新的方法用以擴充對象的行爲。

3.說明

由於具體構件類和裝飾類都實現了相同的抽象構件接口,因此裝飾模式以對客戶端透明的方式動態地給一個對象附加上更多的責任。換言之,客戶端並不會覺得對象在裝飾前和裝飾後有什麼不同。裝飾模式可以在不需要創造更多子類的情況下,將對象的功能加以擴展。

三、完整解決方案

爲了讓系統具有更好地靈活性和可擴展性,克服繼承複用所帶來地問題,A公司開發人員使用裝飾模式來重構圖形界面構件庫地設計,其中部分結類地結構如圖:

Component 充當抽象構件類,其子類 Window、TextBox、ListBox 充當具體構件類。Component 類的另一個子類 ComponentDecorator 充當抽象裝飾類,ComponentDecorator 的子類 ScrollBarDecorator 和 BlackBorderDecorator 充當具體裝飾類。完整代碼如下:

    /// <summary>
    /// 抽象界面構件類:抽象構件類。爲了突出與模式相關的代碼,對原有控件代碼進行了大量的簡化
    /// </summary>
    public abstract class Component
    {
        public abstract void Display();
    }

    /// <summary>
    /// 窗體類:具體構件類
    /// </summary>
    public class Window : Component
    {
        public override void Display()
        {
            Console.WriteLine("顯示窗體");
        }
    }
    
    /// <summary>
    /// 文本框類:具體構件類
    /// </summary>
    public class TextBox : Component
    {
        public override void Display()
        {
            Console.WriteLine("顯示文本框");
        }
    }
    
    /// <summary>
    /// 列表框類:具體構件類
    /// </summary>
    public class ListBox : Component
    {
        public override void Display()
        {
            Console.WriteLine("顯示列表框");
        }
    }
    
    /// <summary>
    /// 構件裝飾類:抽象裝飾類
    /// </summary>
    public class ComponentDecorator : Component
    {
        //位置對抽象構件類型對象的引用
        private Component component;

        /// <summary>
        /// 注入抽象構件類型的對象
        /// </summary>
        /// <param name="component"></param>
        public ComponentDecorator(Component component)
        {
            this.component = component;
        }

        public override void Display()
        {
            component.Display();
        }
    }
    
    /// <summary>
    /// 滾動條裝飾類:具體裝飾類
    /// </summary>
    public class SrollBarDecorator : ComponentDecorator
    {
        public SrollBarDecorator(Component component) : base(component)
        {

        }

        public override void Display()
        {
            this.SetSsrollBar();
            base.Display();
        }

        public void SetSsrollBar()
        {
            Console.WriteLine("爲構件增加滾動條");
        }
    }
    
    /// <summary>
    /// 黑色邊框裝飾類:具體裝飾類
    /// </summary>
    public class BlackBorderDecorator : ComponentDecorator
    {
        public BlackBorderDecorator(Component component) : base(component)
        {

        }

        public override void Display()
        {
            this.SetBlackBorder();
            base.Display();
        }

        public void SetBlackBorder()
        {
            Console.WriteLine("爲構件增加黑色邊框");
        }
    }

客戶端測試代碼:

    class Program
    {
        static void Main(string[] args)
        {
            Component component = new Window();
            Component componentSB = new SrollBarDecorator(component);
            componentSB.Display();
        }
    }

輸出結果:

如果希望得到一個既有滾動條又有黑色邊框的窗體,不需要對原有類庫進行任何修改,只需要將客戶端代碼進行修改如下:

    class Program
    {
        static void Main(string[] args)
        {
            Component component = new Window();
            Component componentSB = new SrollBarDecorator(component);
            Component componentBB = new BlackBorderDecorator(componentSB);
            componentBB.Display();
        }
    }

編譯並運行:

如果需要在原有系統中增加一個新的具體構件類或者新的具體裝飾類,無需修改現有類庫代碼,只需將他們分別作爲抽象構件類或者抽象裝飾類的子類即可。與繼承結構相比,使用裝飾模式之後大大減少了子類的個數,讓系統擴展起來更加方便,而且更容易維護。裝飾模式是取代繼承複用的有效方式之一。

四、裝飾模式注意事項

在使用裝飾模式時,通常需要注意以下幾個問題:

  1. 儘量保持裝飾類的接口與被裝飾類的接口相同。這樣對客戶端而言,無論是裝飾之前的對象還是裝飾之後的對象都可以一致對待。
  2. 儘量保持具體構件類 ConcreteComponent 是一個“輕”類。也就是說,不要把太多的行爲放在具體構件類中,可以通過裝飾類對其進行擴展。
  3. 如果只有一個具體構件類,那麼抽象裝飾類可以作爲該具體構件類的直接子類。

五、裝飾模式總結

裝飾模式降低了系統的耦合度,可以動態的增加或刪除對象的職責,並使得需要裝飾的具體構件類和具體裝飾類獨立變化,以便增加新的具體構件類和具體裝飾類。

1.主要優點

  1. 對於擴展一個對象的功能,裝飾模式比繼承更加靈活,不會導致類的個數急劇增加。
  2. 可以通過一種動態的方式來擴展一個對象的功能。通過配置文件可以在運行時選擇不同的具體裝飾類,從而實現不同的行爲。
  3. 可以對一個對象進行多次裝飾。通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行爲的組合,得到功能更爲強大的對象。
  4. 具體構件類和具體裝飾類可以獨立變化,用戶可以根據需要增加新的具體構件類和具體裝飾類,原有類庫代碼無需改變,符合開閉原則。

2.主要缺點

  1. 使用裝飾模式進行系統設計時將產生很多小對象。這些對象的區別在於它們之間相互連接的方式有所不同,而不是它們的類或者屬性值有所不同。大量小對象的產生勢必會佔用更多的系統資源,在一定程度上影響程序的性能。
  2. 裝飾模式提供了一種比繼承更加靈活機動的解決方案,但同時也意味着比繼承更加易於出錯,排錯也很困難。對於多次裝飾的對象,調試時尋找錯誤可能需要逐級排查,較爲煩瑣。

3.適用場景

  1. 在不影響其它對象的情況下,以動態、透明的方式給單個對象添加職責。
  2. 當不能採用繼承的方式對系統進行擴展或者採用繼承不利於系統擴展和維護時可以使用裝飾模式。不能採用繼承的情況主要有兩類:①.系統中存在大量獨立的擴展,爲支持每一種擴展或者擴展之間的組合將會產生大量的子類,使得子類數目呈爆炸性增長;②.因爲類已定義不能被繼承。

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

示例代碼:

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

參考資料:

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