大衛的Design Patterns學習筆記11:Decorator

一、概述
繼承是對類進行擴展,以提供更多特性的一種基本方法,但是有時候,簡單的繼承可能不能滿足我們的需求。如我們的系統需要提供多種類型的產品:
類型A、類型B、...
同時,這些產品需要支持多種特性:
特性a、特性b、...
以下是兩種可能的實現:
1
、繼承,分別實現類型Aa、類型Ab、類型Ba、類型Bb、...
這種實現方式在類型的數目和所支持特性的數目衆多時會造成“類爆炸”,即會引入太多的類型,並且,這種實現的封裝性也很差,造成客戶代碼編寫十分困難,十分不可取。
2
、修改各類型實現,在其中包含是否支持特性a、特性b、...選項,根據客戶選擇啓用各特性。這種實現是典型的MFC實現方法,但是這種實現只適合特性非常穩定的情況,否則,當特性發生增減時,各類型實現都可能需要修改。
因此,雖然類似的實現屢見不鮮,但以上兩種實現方式由於對特性的變化或者類型的變化過於敏感,無法滿足類型或特性動態變化的設計需求。如果我們可以將類型和特性分別定義,並且根據客戶代碼的需要動態對類型和特性進行組合,則可以克服上述問題。
正如Decorator(裝飾)模式的名字暗示的那樣,Decorator模式可以在我們需要爲對象添加一些附加的功能/特性時發揮作用,除此之外,更爲關鍵的是Decorator模式研究的是如何以對客戶透明的方式動態地給一個對象附加上更多的特性,換言之,客戶端並不會覺得對象在裝飾前和裝飾後有什麼不同。Decorator模式可以在不創造更多子類的情況下,將對象的功能加以擴展。Decorator模式使用原來被裝飾的類的一個子類的實例,把客戶端的調用委派到被裝飾類,Decorator模式的關鍵在於這種擴展是完全透明的。
正因爲Decorator模式可以動態擴展decoratee所具有的特性,有人將其稱爲“動態繼承模式”,該模式基於繼承,但與靜態繼承下進行功能擴展不同,這種擴展可以被動態賦予decoratee。

二、結構
Decorator模式的結構如下圖所示:

1:Decorator模式類圖示意
在上面的類圖中包括以下組成部分:
1
、Component(抽象構件)角色:給出一個抽象接口,以規範準備接收附加責任的對象。 
2
、Concrete Component(具體構件)角色:定義一個將要接收附加責任的類。 
3
、Decorator(裝飾)角色:持有一個Component對象的實例,並定義一個與抽象構件接口一致的接口。 
4
、Concrete Decorator(具體裝飾)角色:負責給構件對象“貼上”附加的責任。

三、應用
在以下情況下可以考慮使用Decorator模式:
1
、在不影響其他對象的情況下,以動態、透明的方式給單個對象添加職責。(見示例)
2
、處理那些可以撤消的職責。(與上面類似,當有這種動態添加撤銷的需求時,可以爲類添加相應的裝飾類成員,但想要撤銷裝飾時,將該成員設置爲NULL即可,同樣,要支持動態切換也很容易)
3
、當不能採用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴展,爲支持每一種組合將產生大量的子類,使得子類數目呈指數增長。另一種情況可能是因爲類定義被隱藏,或類定義不能用於生成子類。(只要你學過排列組合,這一點應該不難理解)

Decorator和Adapter的不同在於前者不改變接口而後者則提供新的接口。可以將Decorator視爲一個退化的、僅有一個組件的Composite,然而,Decorator的目的在於給對象添加一些額外的職責,而不是對象聚集。

既然Decorator模式如此強大,是不是可以大加推廣,大量運用Decorator來替代簡單的繼承呢?這樣,直接通過繼承來擴展類的功能就可以退出歷史舞臺了!實際上這是不可能的,主要原因如下:
1
、雖然“繼承破壞了封裝性”(父類向子類開放了過多的權限),但是,繼承關係是客觀世界及OOP中最基本的關係,而且,繼承是深化接口規範的基礎,沒有繼承就沒有多態等諸多OO特性,因此,繼承比Decorator更常見,也更容易定義和實現;
2
、由於Decorator動態疊加及不影響decoratee等特性的要求,Decorator很難用於複雜特性的定義;
3
、Decorator是一種聚合與繼承的結合,應用Decorate模式還存在着其它一些限制,具體將在實現舉例部分討論。

四、優缺點
使用裝飾模式主要有以下的優點:
1
、裝飾模式與繼承關係的目的都是要擴展對象的功能,但是裝飾模式可以提供比繼承更多的靈活性。 
2
、通過使用不同的具體裝飾類以及這些裝飾類的排列組合,設計師可以創造出很多不同行爲的組合。 

使用裝飾模式主要有以下的缺點:
由於使用裝飾模式,可以比使用繼承關係需要較少數目的類,使用較少的類,固然使設計比較易於進行,但是,在另一方面,Decorator的缺點是會產生一些極爲類似的小型對象,這些小型對象是爲了提供極少量的特殊功能而定製的。

五、舉例
Decorator模式基本的實現方式如下:Decorator類從待修飾類ConcreteComponent的基類Component派生,以便與ConcreteComponent保持相同的接口,同時,在內部將所有函數調用轉發給內部包容的ConcreteComponent對象來執行(1、被包容的ConcreteComponent對象通過Decorator的構造函數傳入。2、往往會附加一些“修飾”,否則,Decorator就徒有虛名了)。

在下面的例子中,xsstream用於對sstream進行Decorate,以統計調用operator <<的次數,示例代碼如下:

#include <iostream>
using namespace std;

struct
 stream
{

    virtual
 stream& operator <<(int i) = 0;
    virtual
 stream& operator <<(const char* str) = 0;
};


struct
 sstream : public stream
{

    stream& operator <<(int i)
    {

        cout << i;
        return
 *this;
    }

    stream& operator <<(const char* str)
    {

        cout << str;
        return
 *this;
    }
};


class
 xsstream : public stream
{

    static
 int count;
    stream* ps;
public
:
    xsstream(stream* s) : ps(s) {}

    stream& operator <<(int i) {
        *
ps << "This is [" << ++count << "] call to operator <<. i = " << i << "/n";
        return
 *this;
    }

    stream& operator <<(const char* str) {
        *
ps << "This is [" << ++count << "] call to operator <<. str = " << str << "/n";
        return
 *this;
    }
};

int
 xsstream::count = 0;

int
 main() {
    sstream ss;
    stream* ps = new xsstream(&ss);
    stream& s = *ps;

    int
 i = 1;
    s << i << "abc";

    delete
 ps;

    return
 0;
}


以上方法實現的Decorator模式實質上是對包容的擴展(由於只有一個ConcreteComponent,它看起來很像Proxy模式,但意圖不同),雖然以上示例沒有什麼應用價值,但它基本闡明瞭Decorator實現的基本方法:
重新定義Decoratee中的接口方法(由於Decorator與Decoratee從相同基類派生,所以這是可能的),在其中添加必要的Decoration,並調用Decoratee的相應方法完成Decoration以外的工作,對於無需修飾的輔助方法,可以直接將方法調用轉發給Decoratee。

但是,上述實現同時也告訴了我們Decorator模式存在的一個非常重要的限制,就是:
如果我們從抽象基類派生,我們必須實現抽象基類的每一個虛方法(或者,對於非虛基類,要麼重載基類的方法,要麼使用基類的實現,但這就丟失了ConcreteComponent子類中重載的實現,即失去(-)了特性,這與Decorator模式進行修飾,即加(+)特性的實質有悖),而當虛方法的數目衆多時,這將成爲一種負擔。如實際basic_ostream實現的operator <<有多種,分別用於boolshortunsigned shortlong、longlong...等等,逐一實現它們是一件很繁瑣的事情。
要解決這一問題,可以從sstream而不是stream派生,當我們只需要裝飾已經實現的一部分方法時這一招似乎“很管用”,但這有悖於Decorator模式的初衷。因爲,Decorator模式提出的目的在於修飾一個類系,而不是單個的類。如果單純的爲了修飾單個的類,簡單的繼承擴展即可解決問題,根本就無需從Decorator的角度來考慮這個問題,而如果面對的是多個類,這種方式使我們必須再次面對“類組合爆炸”的窘境。
幸好以上這個問題對於基於消息/事件的應用中不存在,如在MFC中,進行界面修飾時,可以只需要特別修飾的消息進行處理,而將所有其它消息轉發給待修飾的控件。
這麼看來Decorator模式似乎在MFC應用中大有用武之地,但事實並非如此,之所以出現這種局面,大概是因爲其一Decorator模式會使得客戶代碼變得複雜,我們必須自己創建特性,而不是在Create時直接指定,MFC的實現者希望MFC封裝類保持與API一樣的接口,在將特性在創建控件時靜態指定與由用戶動態創建並指定之間,MFC的實現者選擇了前者;其二,從上面可以看出,Decorator模式對於小特性的定義比較合適,當特性或類系十分複雜時,Decorator模式很難做到面面俱到。

在Java中,java.io.LineNumberReader是一個典型的Decorator,可以Decorate整個Reader類系,用於在讀取文件信息的同時統計LineNumber信息,你可以在readLine後通過LineNumberReader.getLineNumber方法獲取當前的行號,其實現比較簡單,有興趣的朋友可以研究一下。

參考:
1
、Java中Decorate的三種實現方法:http://www.china-dev.com/2004/04/17/10115.html
注:上文講述的是Decorate,而不是Decorator,如果將Decorate改成Decorator,則部分觀點是不恰當的,如JScrollPane修飾JTextArea,嚴格來講應用的是組合模式,而不是Decorator模式,閱讀時需注意。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章