Java設計模式12:裝飾器模式

五月的倉頡發表於2015-10-25

裝飾器模式

裝飾器模式又稱為包裝(Wrapper)模式。裝飾器模式以多客戶端透明的方式擴充套件物件的功能,是繼承關係的一個替代方案

 

裝飾器模式的結構

通常給物件新增功能,要麼直接修改物件新增相應的功能,要麼派生子類來擴充套件,抑或是使用物件組合的方式。顯然,直接修改對應的類的方式並不可取,在物件導向的設計中,我們應該儘量使用組合物件而不是繼承物件來擴充套件和複用功能,裝飾器模式就是基於物件組合的方式的。

裝飾器模式以對客戶端透明的方式動態地給一個物件附加上了更多的責任。換言之,客戶端並不會角色物件在裝飾前和裝飾後有什麼不同。裝飾器模式可以在不用建立更多子類的情況下,將物件的功能加以擴充套件。

裝飾器模式中的角色有:

1、抽象構件角色

給出一個抽象介面,以規範準備接受附加責任的物件

2、具體構件角色

定義一個將要接受附加責任的類

3、裝飾角色

持有一個構建物件的例項,並定義一個與抽象構件介面一致的介面

4、具體裝飾角色

負責給構建物件貼上附加的責任

 

裝飾器模式的例子

現在有這麼一個場景:

1、有一批廚師,簡單點吧,就全是中國廚師,他們有一個共同的動作是做晚飯

2、這批廚師做晚飯前的習慣不同,有些人喜歡做晚飯前洗手、有些人喜歡做晚飯前洗頭

那麼,按照裝飾器模式,先抽象出抽象構建角色,Cook介面:

public interface Cook {

    public void cookDinner();
    
}

具體構建角色,中國廚師:

public class ChineseCook implements Cook {

    @Override
    public void cookDinner() {
        System.out.println("中國人做晚飯");
    }
    
}

定義一個裝飾器角色,具體的工作具體裝飾器去實現,這樣,比如美國廚師做晚飯前也先洗手或者先洗頭,這兩個動作就可以做到複用,裝飾器角色定義為FilterCook,很簡單,實現Cook介面並持有Cook的引用:

public abstract class FilterCook implements Cook {

    protected Cook cook;
    
}

最後定義一個具體裝飾角色,該洗手的洗手,該洗頭的洗頭:

public class WashHandsCook extends FilterCook {

    public WashHandsCook(Cook cook) {
        this.cook = cook;
    }
    
    @Override
    public void cookDinner() {
        System.out.println("先洗手");
        cook.cookDinner();
    }
    
}
public class WashHearCook extends FilterCook {
    
    public WashHearCook(Cook cook) {
        this.cook = cook;
    }
    
    @Override
    public void cookDinner() {
        System.out.println("先洗頭");
        cook.cookDinner();
    }
    
}

呼叫方這麼實現:

@Test
public void testDecorate() {
    Cook cook0 = new WashHandsCook(new ChineseCook());
    Cook cook1 = new WashHearCook(new ChineseCook());
        
    cook0.cookDinner();
    cook1.cookDinner();
}

執行結果為:

先洗手
中國人做飯
先洗頭
中國人做飯

簡單的一個例子,實現了裝飾器模式的兩個功能點:

  1. 客戶端只定義了Cook介面,並不關心具體實現
  2. 給Chinese增加上了洗頭和洗手的動作,且洗頭和洗手的動作,可以給其他國家的廚師類複用

這就是裝飾器模式。

 

裝飾器模式與Java位元組輸入流InputStream

上面的例子可能寫得不是很清楚,因此這裡再繼續用程式碼示例講解裝飾器模式。

裝飾器模式在Java體系中的經典應用是Java I/O,下面先講解位元組輸入流InputStream,再講解字元輸入流Reader,希望可以通過這兩種輸入流的講解,加深對於裝飾器模式的理解。

首先看一下位元組輸入流InputStream的類結構體系:

InputStream是一個頂層的介面,文章開頭就說,裝飾器模式是繼承關係的一種替代方案,看一下為什麼:

  1. InputStream假設這裡寫了兩個實現類,FileInputStream,ObjectInputStream分別表示檔案位元組輸入流,物件位元組輸入流
  2. 現在我要給這兩個輸入流加入一點緩衝功能以提高輸入流效率,使用繼承的方式,那麼就寫一個BufferedInputStream,繼承FileInputStream,ObjectInputStream,給它們加功能
  3. 現在我有另外一個需求,需要給這兩個輸入流加入一點網路功能,那麼就寫一個SocketInputStream,繼承繼承FileInputStream,ObjectInputStream,給它們加功能

這樣就導致兩個問題:

  1. 因為我要給哪個類加功能就必須繼承它,比如我要給FileInputStream,ObjectInputStream加上緩衝功能、網路功能就得擴充套件出2*2=4個類,更多的以此類推,這樣勢必導致類數量不斷膨脹
  2. 程式碼無法複用,給FileInputStream,ObjectInputStream加入緩衝功能,本身程式碼應該是一樣的,現在卻必須繼承完畢後把一樣的程式碼重寫一遍,多此一舉,程式碼修改的時候必須修改多個地方,可維護性很差

所以,這個的時候我們就想到了一種解決方案:

  1. 在要擴充套件的類比如BufferedInputStream中持有一個InputStream的引用,在BufferedInputStream呼叫InputStream中的方法,這樣擴充套件的程式碼就可以複用起來
  2. 將BufferedInputStream作為InputStream的子類,這樣客戶端只知道我用的是InputStream而不需要關心具體實現,可以在客戶端不知情的情況下,擴充套件InputStream的功能,加上緩衝功能

這就是裝飾器模式簡單的由來,一切都是為了解決實際問題而誕生。下一步,根據UML圖,我們來劃分一下裝飾器模式的角色。

1、InputStream是一個抽象構件角色:

public abstract class InputStream implements Closeable {

    // SKIP_BUFFER_SIZE is used to determine the size of skipBuffer
    private static final int SKIP_BUFFER_SIZE = 2048;
    // skipBuffer is initialized in skip(long), if needed.
    private static byte[] skipBuffer;
    ...
}

2、ByteArrayInputStream、FileInputStream、ObjectInputStream、PipedInputStream都是具體構建角色,比如FileInputStream,它的宣告是:

public
class FileInputStream extends InputStream
{
    /* File Descriptor - handle to the open file */
    private FileDescriptor fd;

    private FileChannel channel = null;
    ...
}

3、FilterInputStream無疑就是一個裝飾角色,因為FilterInputStream實現了InputStream內的所有抽象方法並且持有一個InputStream的引用:

public
class FilterInputStream extends InputStream {
    /**
     * The input stream to be filtered. 
     */
    protected volatile InputStream in;
    ...
}

4、具體裝飾角色就是InflaterInputStream、BufferedInputStream、DataInputStream,比如BufferedInputStream的宣告就是:

public
class BufferedInputStream extends FilterInputStream {

    private static int defaultBufferSize = 8192;

    /**
     * The internal buffer array where the data is stored. When necessary,
     * it may be replaced by another array of
     * a different size.
     */
    protected volatile byte buf[];
    ...
}

搞清楚具體角色之後,我們就可以這麼寫了:

public static void main(String[] args) throws Exception
{
    File file = new File("D:/aaa.txt");
    InputStream in0 = new FileInputStream(file);
    InputStream in1 = new BufferedInputStream(new FileInputStream(file)); 
    InputStream in2 = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
}

我們這裡例項化出了三個InputStream的實現類:

  1. in0這個引用指向的是new出來的FileInputStream,這裡簡單構造出了一個檔案位元組輸入流
  2. in1這個引用指向的是new出來的BufferedInputStream,它給FileInputStream增加了緩衝功能,使得FileInputStream讀取檔案的內容儲存在記憶體中,以提高讀取的功能
  3. in2這個引用指向的是new出來的DataInputStream,它也給FileInputStream增加了功能,因為它有DataInputStream和BufferedInputStream兩個附加的功能

同理,我要給ByteArrayInputStream、ObjectInputStream增加功能,也可以使用類似的方法,整個過程中,最重要的是要理解幾個問題:

  1. 哪些是具體構建角色、哪些是具體裝飾角色,尤其是後者,區分的關鍵就是,角色中是否持有頂層介面的引用
  2. 每個具體裝飾角色有什麼作用,因為只有知道每個具體裝飾角色有什麼作用後,才可以知道要裝飾某個功能需要用哪個具體裝飾角色
  3. 使用構造方法的方式將類進行組合,給具體構建角色加入新的功能

 

裝飾器模式與Java字元輸入流Reader

看完了上面的解讀,相信大家對於裝飾器模式應當有了一定的理解,那麼再來看一下Java字元輸入流Reader,來加深對於裝飾器模式的印象。

簡單看一下Reader的類體系結構:

根據UML,分析一下每個角色:

1、抽象構建角色

毫無疑問,由Reader來扮演,它是一個抽象類,沒有具體功能

2、具體構建角色

由InputStreamReader、CharArrayReader、PipedReader、StringReader來扮演

3、裝飾角色

由FilterReader來扮演,但是這裡要提一下這個BufferedReader,它本身也可以作為裝飾角色出現,看一下BufferedReader的繼承關係:

public class BufferedReader extends Reader {

    private Reader in;

    private char cb[];
    private int nChars, nextChar;

    private static final int INVALIDATED = -2;
    private static final int UNMARKED = -1;
    private int markedChar = UNMARKED;
    
    ...
}

看到BufferedReader是Reader的子類,且持有Reader的引用,因此這裡的BufferedReader是可以被認為是一個裝飾角色的。

4、具體裝飾角色

BufferedReader上面提到了扮演了裝飾角色,但是也可以被認為是一個具體裝飾角色。除了BufferedReader,具體裝飾角色還有PushbackReader。FileReader儘管也在第三行,但是FileReader構不成一個具體裝飾角色,因為它不是BufferedReader的子類也不是FilterReader的子類,不持有Reader的引用

 

半透明裝飾器模式與全透明裝飾器模式

再說一下半透明裝飾器模式與全透明裝飾器模式,它們的區別是:

  1. 對於半透明裝飾器模式,裝飾後的類未必有和抽象構件角色同樣的介面方法,它可以有自己擴充套件的方法
  2. 對於全透明裝飾器模式,裝飾後的類有著和抽象構件角色同樣的介面方法

全透明裝飾器模式是一種比較理想主義的想法,現實中不太可能出現。

比如BufferedInputStream吧,我把FileInputStream裝飾為BufferedInputStream,難道BufferedInputStream就完全沒有自己的行為?比如返回緩衝區的大小、清空緩衝區(這裡只是舉個例子,實際BufferedInputStream是沒有這兩個動作的),這些都是InputStream本身不具備的,因為InputStream根本不知道緩衝區這個概念,它只知道定義讀資料相關方法。

所以,更多的我們是採用半透明的裝飾器模式,即允許裝飾後的類中有屬於自己的方法,因此,前面的I/O程式碼示例可以這麼改動:

public static void main(String[] args) throws Exception
{
    File file = new File("D:/aaa.txt");
    FileInputStream in0 = new FileInputStream(file);
    BufferedInputStream in1 = new BufferedInputStream(new FileInputStream(file)); 
    DataInputStream in2 = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
}

這樣才更有現實意義。

 

裝飾器模式的優缺點

優點

1、裝飾器模式與繼承關係的目的都是要擴充套件物件的功能,但是裝飾器模式可以提供比繼承更多的靈活性。裝飾器模式允許系統動態決定貼上一個需要的裝飾,或者除掉一個不需要的裝飾。繼承關係是不同,繼承關係是靜態的,它在系統執行前就決定了

2、通過使用不同的具體裝飾器以及這些裝飾類的排列組合,設計師可以創造出很多不同的行為組合

缺點

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

 

裝飾器模式和介面卡模式的區別

其實介面卡模式也是一種包裝(Wrapper)模式,它們看似都是起到包裝一個類或物件的作用,但是它們使用的目的非常不一樣:

1、介面卡模式的意義是要將一個介面轉變成另外一個介面,它的目的是通過改變介面來達到重複使用的目的

2、裝飾器模式不要改變被裝飾物件的介面,而是恰恰要保持原有的藉口哦,但是增強原有介面的功能,或者改變元有物件的處理方法而提升效能

所以這兩種設計模式的目的是不同的。

 

相關文章