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


裝飾器模式

裝飾器模式又稱為包裝(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、裝飾器模式不要改變被裝飾對象的接口,而是恰恰要保持原有的借口哦,但是增強原有接口的功能,或者改變元有對象的處理方法而提升性能

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM