結構型模式3:裝飾模式(Decorator Pattern)

概述

在軟件系統中,有時候我們會使用繼承來擴展對象的功能,但是由於繼承爲類型引入的靜態特質,使得這種擴展方式缺乏靈活性;並且隨着子類的增多(擴展功能的增多),各種子類的組合(擴展功能的組合)會導致更多子類的膨脹。如何使“對象功能的擴展”能夠根據需要來動態地實現?同時避免“擴展功能的增多”帶來的子類膨脹問題?從而使得任何“功能擴展變化”所導致的影響將爲最低?這就是本文要講的Decorator模式。

意圖

動態地給一個對象添加一些額外的職責。就增加功能來說,Decorator模式相比生成子類更爲靈活。[GOF 《設計模式》]

結構圖

圖1 Decorator模式結構圖

生活中的例子

裝飾模式動態地給一個對象添加額外的職責。不論一幅畫有沒有畫框都可以掛在牆上,但是通常都是有畫框的,並且實際上是畫框被掛在牆上。在掛在牆上之前,畫可以被蒙上玻璃,裝到框子裏;這時畫、玻璃和畫框形成了一個物體。

圖2 使用有畫框的畫作爲例子的裝飾模式對象圖

裝飾模式解說

在軟件開發中,經常會遇到動態地爲一個對象而不是整個類增加一些功能的問題,還是以我慣用的記錄日誌的例子來說明吧(也許在Decorator模式裏面用這個例子不是特別合適)。現在要求我們開發的記錄日誌的組件,除了要支持數據庫記錄DatabaseLog和文本文件記錄TextFileLog兩種方式外,我們還需要在不同的應用環境中增加一些額外的功能,比如需要記錄日誌信息的錯誤嚴重級別,需要記錄日誌信息的優先級別,還有日誌信息的擴展屬性等功能。在這裏,如果我們不去考慮設計模式,解決問題的方法其實很簡單,可以通過繼承機制去實現,日誌類結構圖如下:

圖3

實現代碼如下:

public abstract class Log

{

    public abstract void Write(string log);

}

public class DatabaseLog : Log

{

    public override void Write(string log)

    {

        //......記錄到數據庫中

    }

}

public class TextFileLog : Log

{

    public override void Write(string log)

    {

        //......記錄到文本文件中

    }

}

需要記錄日誌信息的錯誤嚴重級別功能和記錄日誌信息優先級別的功能,只要在原來子類DatabaseLog和TextFileLog的基礎上再生成子類即可,同時需要引進兩個新的接口IError和I Priority,類結構圖如下:

圖4

實現代碼如下:

public interface IError

{

    void SetError();

}

public interface IPriority

{

    void SetPriority();

}

public class DBErrorLog : DatabaseLogIError

{

    public override void Write(string log)

    {

        base.Write(log);

    }

    public void SetError()

    {

       //......功能擴展,實現了記錄錯誤嚴重級別

    }

}

public class DBPriorityLog : DatabaseLogIPriority

{

    public override void Write(string log)

    {

        base.Write(log);

    }

    public void SetPriority()

    {

        //......功能擴展,實現了記錄優先級別

    }

}

public class TFErrorLog : TextFileLogIError

{

    public override void Write(string log)

    {

        base.Write(log);

    }

    public void SetError()

    {

        //......功能擴展,實現了記錄錯誤嚴重級別

    }

}

public class TFPriorityLog : TextFileLogIPriority

{

    public override void Write(string log)

    {

        base.Write(log);

    }

    public void SetPriority()

    {

        //......功能擴展,實現了記錄優先級別

    }

}

此時可以看到,如果需要相應的功能,直接使用這些子類就可以了。這裏我們採用了類的繼承方式來解決了對象功能的擴展問題,這種方式是可以達到我們預期的目的。然而,它卻帶來了一系列的問題。首先,前面的分析只是進行了一種功能的擴展,如果既需要記錄錯誤嚴重級別,又需要記錄優先級時,子類就需要進行接口的多重繼承,這在某些情況下會違反類的單一職責原則,注意下圖中的藍色區域:

圖5

實現代碼:

public class DBEPLog : DatabaseLogIErrorIPriority

{

    public override void Write(string log)

    {

        SetError();

        SetPriority();

        base.Write(log);

    }

    public void SetError()

    {

        //......功能擴展,實現了記錄錯誤嚴重級別

    }

    public void SetPriority()

    {

        //......功能擴展,實現了記錄優先級別

    }

}

public class TFEPLog : DatabaseLogIErrorIPriority

{

    public override void Write(string log)

    {

        SetError();

        SetPriority();

base.Write(log);

    }

    public void SetError()

    {

        //......功能擴展,實現了記錄錯誤嚴重級別

    }

    public void SetPriority()

    {

        //......功能擴展,實現了記錄優先級別

    }

}

其次,隨着以後擴展功能的增多,子類會迅速的膨脹,可以看到,子類的出現其實是DatabaseLog和TextFileLog兩個子類與新增加的接口的一種排列組合關係,所以類結構會變得很複雜而難以維護,正如象李建忠老師說的那樣“子類復子類,子類何其多”;最後,這種方式的擴展是一種靜態的擴展方式,並沒有能夠真正實現擴展功能的動態添加,客戶程序不能選擇添加擴展功能的方式和時機。

現在又該是Decorator模式出場的時候了,解決方案是把Log對象嵌入到另一個對象中,由這個對象來擴展功能。首先我們要定義一個抽象的包裝類LogWrapper,讓它繼承於Log類,結構圖如下:

圖6

實現代碼如下:

public abstract class LogWrapper : Log

{

    private Log _log;

    public LogWrapper(Log log)

    {

        _log = log;

    }

    public override void Write(string log)

    {

        _log.Write(log);

    }

}

現在對於每個擴展的功能,都增加一個包裝類的子類,讓它們來實現具體的擴展功能,如下圖中綠色的區域:

圖7

實現代碼如下:

public class LogErrorWrapper : LogWrapper

{

    public LogErrorWrapper(Log _log)

        :base(_log)

    {

       

    }

    public override void Write(string log)

    {

        SetError(); //......功能擴展

 

        base.Write(log);

    }

    public void SetError()

    {

        //......實現了記錄錯誤嚴重級別

    }

}

public class LogPriorityWrapper : LogWrapper

{

    public LogPriorityWrapper(Log _log)

        : base(_log)

    {

 

    }

    public override void Write(string log)

    {

        SetPriority(); //......功能擴展

 

        base.Write(log);

    }

    public void SetPriority()

    {

        //......實現了記錄優先級別

    }

}

到這裏,LogErrorWrapper類和LogPriorityWrapper類真正實現了對錯誤嚴重級別和優先級別的功能的擴展。我們來看一下客戶程序如何去調用它:

public class Program

{

    public static void Main(string[] args)

    {

        Log log = new DatabaseLog();

        LogWrapper lew1 = new LogErrorWrapper(log);

        //擴展了記錄錯誤嚴重級別

        lew1.Write("Log Message");

 

        LogPriorityWrapper lpw1 = new LogPriorityWrapper(log);

        //擴展了記錄優先級別

        lpw1.Write("Log Message");

 

        LogWrapper lew2 = new LogErrorWrapper(log);

        LogPriorityWrapper lpw2 = new LogPriorityWrapper(lew2); //這裏是lew2

        //同時擴展了錯誤嚴重級別和優先級別

        lpw2.Write("Log Message");

    }

}

注意在上面程序中的第三段裝飾才真正體現出了Decorator模式的精妙所在,這裏總共包裝了兩次:第一次對log對象進行錯誤嚴重級別的裝飾,變成了lew2對象,第二次再對lew2對象進行裝飾,於是變成了lpw2對象,此時的lpw2對象同時擴展了錯誤嚴重級別和優先級別的功能。也就是說我們需要哪些功能,就可以這樣繼續包裝下去。到這裏也許有人會說LogPriorityWrapper類的構造函數接收的是一個Log對象,爲什麼這裏可以傳入LogErrorWrapper對象呢?通過類結構圖就能發現,LogErrorWrapper類其實也是Log類的一個子類。

我們分析一下這樣會帶來什麼好處?首先對於擴展功能已經實現了真正的動態增加,只在需要某種功能的時候才進行包裝;其次,如果再出現一種新的擴展功能,只需要增加一個對應的包裝子類(注意:這一點任何時候都是避免不了的),而無需再進行很多子類的繼承,不會出現子類的膨脹,同時Decorator模式也很好的符合了面向對象設計原則中的“優先使用對象組合而非繼承”和“開放-封閉”原則。

.NET中的裝飾模式

1..NET中Decorator模式一個典型的運用就是關於Stream,它存在着如下的類結構:

圖8

可以看到, BufferedStream和CryptoStream其實就是兩個包裝類,這裏的Decorator模式省略了抽象裝飾角色(Decorator),示例代碼如下:

class Program

{

    public static void Main(string[] args)

    {

        MemoryStream ms =

            new MemoryStream(new byte[] { 100,456,864,222,567});

 

        //擴展了緩衝的功能

        BufferedStream buff = new BufferedStream(ms);

 

        //擴展了緩衝,加密的功能

        CryptoStream crypto = new CryptoStream(buff);

    }

}

通過反編譯,可以看到BufferedStream類的代碼(只列出部分),它是繼承於Stream類:

public sealed class BufferedStream : Stream

{

    // Methods

    private BufferedStream();

    public BufferedStream(Stream stream);

    public BufferedStream(Stream stream, int bufferSize);

    // Fields

    private int _bufferSize;

    private Stream _s;

}

2.在Enterprise Library中的DAAB中有一個DbCommandWrapper的包裝類,它實現了對IDbCommand類的包裝並提供了參數處理的功能。結構圖如下:

圖9

示意性代碼如下:

public abstract class DBCommandWrapper : MarshalByRefObjectIDisposable

{

 

}

public class SqlCommandWrapper : DBCommandWrapper

{

   

}

public class OracleCommandWrapper : DBCommandWrapper

{

 

}

效果及實現要點

1.Component類在Decorator模式中充當抽象接口的角色,不應該去實現具體的行爲。而且Decorator類對於Component類應該透明,換言之Component類無需知道Decorator類,Decorator類是從外部來擴展Component類的功能。

2.Decorator類在接口上表現爲is-a Component的繼承關係,即Decorator類繼承了Component類所具有的接口。但在實現上又表現爲has-a Component的組合關係,即Decorator類又使用了另外一個Component類。我們可以使用一個或者多個Decorator對象來“裝飾”一個Component對象,且裝飾後的對象仍然是一個Component對象。

3.Decortor模式並非解決“多子類衍生的多繼承”問題,Decorator模式的應用要點在於解決“主體類在多個方向上的擴展功能”——是爲“裝飾”的含義。

4.對於Decorator模式在實際中的運用可以很靈活。如果只有一個ConcreteComponent類而沒有抽象的Component類,那麼Decorator類可以是ConcreteComponent的一個子類。

圖10

如果只有一個ConcreteDecorator類,那麼就沒有必要建立一個單獨的Decorator類,而可以把Decorator和ConcreteDecorator的責任合併成一個類。

圖11

5.Decorator模式的優點是提供了比繼承更加靈活的擴展,通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行爲的組合。

6.由於使用裝飾模式,可以比使用繼承關係需要較少數目的類。使用較少的類,當然使設計比較易於進行。但是,在另一方面,使用裝飾模式會產生比使用繼承關係更多的對象。更多的對象會使得查錯變得困難,特別是這些對象看上去都很相像。

適用性

在以下情況下應當使用裝飾模式:

1.需要擴展一個類的功能,或給一個類增加附加責任。

2.需要動態地給一個對象增加功能,這些功能可以再動態地撤銷。

3.需要增加由一些基本功能的排列組合而產生的非常大量的功能,從而使繼承關係變得不現實。

總結

Decorator模式採用對象組合而非繼承的手法,實現了在運行時動態的擴展對象功能的能力,而且可以根據需要擴展多個功能,避免了單獨使用繼承帶來的“靈活性差”和“多子類衍生問題”。同時它很好地符合面向對象設計原則中“優先使用對象組合而非繼承”和“開放-封閉”原則。

參考資料

閻宏,《Java與模式》,電子工業出版社

James W. Cooper,《C#設計模式》,電子工業出版社

Alan Shalloway James R. Trott,《Design Patterns Explained》,中國電力出版社

MSDN WebCast 《C#面向對象設計模式縱橫談(10) Decorator裝飾模式(結構型模式)》

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