天天看點

.NET設計模式(10):裝飾模式(Decorator Pattern)

——.NET設計模式系列之十

Terrylee,2006年3月

<b>概述</b><b></b>

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

<b>意圖</b>

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

<b>結構圖</b><b></b>

.NET設計模式(10):裝飾模式(Decorator Pattern)

圖1 Decorator模式結構圖

<b>生活中的例子</b><b></b>

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

.NET設計模式(10):裝飾模式(Decorator Pattern)

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

<b>裝飾模式解說</b><b></b>

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

.NET設計模式(10):裝飾模式(Decorator Pattern)

圖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

        //......記錄到文本檔案中

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

.NET設計模式(10):裝飾模式(Decorator Pattern)

圖4

public interface IError

    void SetError();

public interface IPriority

    void SetPriority();

public class DBErrorLog : DatabaseLog, IError

        base.Write(log);

    public void SetError()

       //......功能擴充,實作了記錄錯誤嚴重級别

public class DBPriorityLog : DatabaseLog, IPriority

    public void SetPriority()

        //......功能擴充,實作了記錄優先級别

public class TFErrorLog : TextFileLog, IError

        //......功能擴充,實作了記錄錯誤嚴重級别

public class TFPriorityLog : TextFileLog, IPriority

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

.NET設計模式(10):裝飾模式(Decorator Pattern)

圖5

實作代碼:

public class DBEPLog : DatabaseLog, IError, IPriority

        SetError();

        SetPriority();

public class TFEPLog : DatabaseLog, IError, IPriority

        SetError();

base.Write(log);

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

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

.NET設計模式(10):裝飾模式(Decorator Pattern)

圖6

public abstract class LogWrapper : Log

    private Log _log;

    public LogWrapper(Log log)

        _log = log;

        _log.Write(log);

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

.NET設計模式(10):裝飾模式(Decorator Pattern)

圖7

public class LogErrorWrapper : LogWrapper

    public LogErrorWrapper(Log _log)

        :base(_log)

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

        //......實作了記錄錯誤嚴重級别

public class LogPriorityWrapper : LogWrapper

    public LogPriorityWrapper(Log _log)

        : base(_log)

        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模式也很好的符合了面向對象設計原則中的“優先使用對象組合而非繼承”和“開放-封閉”原則。

<b>.NET</b><b>中的裝飾模式</b><b></b>

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

.NET設計模式(10):裝飾模式(Decorator Pattern)

圖8

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

class Program

        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類的包裝并提供了參數處理的功能。結構圖如下:

.NET設計模式(10):裝飾模式(Decorator Pattern)

圖9

示意性代碼如下:

public abstract class DBCommandWrapper : MarshalByRefObject, IDisposable

public class SqlCommandWrapper : DBCommandWrapper

public class OracleCommandWrapper : DBCommandWrapper

<b>效果及實作要點</b><b></b>

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的一個子類。

.NET設計模式(10):裝飾模式(Decorator Pattern)

圖10

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

圖11

.NET設計模式(10):裝飾模式(Decorator Pattern)

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

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

<b>适用性</b><b></b>

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

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

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

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

<b>總結</b><b></b>

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

<b>參考資料</b><b></b>

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

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

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

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