(十)裝飾器模式詳解(與IO不解的情緣)


                 作者:zuoxiaolong8810(左瀟龍),轉載請注明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特復制到此留存,如需轉載請注明新博客地址即可。

                 LZ到目前已經寫了九個設計模式,回過去看看,貌似寫的有點凌亂,LZ后面會盡量改進。

                 那么本章LZ和各位讀友討論一個與JAVA中IO有着不解情緣的設計模式,裝飾器模式。

                 定義:裝飾模式是在不必改變原類文件和使用繼承的情況下,動態的擴展一個對象的功能。它是通過創建一個包裝對象,也就是裝飾來包裹真實的對象。

                 這一個解釋,引自百度百科,我們注意其中的幾點。

                 1,不改變原類文件。

                 2,不使用繼承。

                 3,動態擴展。

                 上述三句話一語道出了裝飾器模式的特點,下面LZ給出裝飾器模式的類圖,先上圖再解釋。


                  從圖中可以看到,我們裝飾的是一個接口的任何實現類,而這些實現類也包括了裝飾器本身,裝飾器本身也可以再被裝飾。

                  另外,這個類圖只是裝飾器模式的完整結構,但其實里面有很多可以變化的地方,LZ給出如下兩條。

                  1,Component接口可以是接口也可以是抽象類,甚至是一個普通的父類(這個強烈不推薦,普通的類作為繼承體系的超級父類不易於維護)。

                  2,裝飾器的抽象父類Decorator並不是必須的。

                 那么我們將上述標准的裝飾器模式,用我們熟悉的JAVA代碼給詮釋一下。首先是待裝飾的接口Component。

package com.decorator;

public interface Component {

    void method();
    
}

                 接下來便是我們的一個具體的接口實現類,也就是俗稱的原始對象,或者說待裝飾對象。

package com.decorator;

public class ConcreteComponent implements Component{

    public void method() {
        System.out.println("原來的方法");
    }

}

                 下面便是我們的抽象裝飾器父類,它主要是為裝飾器定義了我們需要裝飾的目標是什么,並對Component進行了基礎的裝飾。

package com.decorator;

public abstract class Decorator implements Component{

    protected Component component;

    public Decorator(Component component) {
        super();
        this.component = component;
    }

    public void method() {
        component.method();
    }
    
}

                  再來便是我們具體的裝飾器A和裝飾器B。

package com.decorator;

public class ConcreteDecoratorA extends Decorator{

    public ConcreteDecoratorA(Component component) {
        super(component);
    }
    
    public void methodA(){
        System.out.println("被裝飾器A擴展的功能");
    }

    public void method(){
        System.out.println("針對該方法加一層A包裝");
        super.method();
        System.out.println("A包裝結束");
    }
}
package com.decorator;

public class ConcreteDecoratorB extends Decorator{

    public ConcreteDecoratorB(Component component) {
        super(component);
    }
    
    public void methodB(){
        System.out.println("被裝飾器B擴展的功能");
    }

    public void method(){
        System.out.println("針對該方法加一層B包裝");
        super.method();
        System.out.println("B包裝結束");
    }
}

                下面給出我們的測試類。我們針對多種情況進行包裝。

package com.decorator;

public class Main {

    public static void main(String[] args) {
        Component component =new ConcreteComponent();//原來的對象
        System.out.println("------------------------------");
        component.method();//原來的方法
        ConcreteDecoratorA concreteDecoratorA = new ConcreteDecoratorA(component);//裝飾成A
        System.out.println("------------------------------");
        concreteDecoratorA.method();//原來的方法
        concreteDecoratorA.methodA();//裝飾成A以后新增的方法
        ConcreteDecoratorB concreteDecoratorB = new ConcreteDecoratorB(component);//裝飾成B
        System.out.println("------------------------------");
        concreteDecoratorB.method();//原來的方法
        concreteDecoratorB.methodB();//裝飾成B以后新增的方法
        concreteDecoratorB = new ConcreteDecoratorB(concreteDecoratorA);//裝飾成A以后再裝飾成B
        System.out.println("------------------------------");
        concreteDecoratorB.method();//原來的方法
        concreteDecoratorB.methodB();//裝飾成B以后新增的方法
    }
}

                 下面看下我們運行的結果,到底是產生了什么效果。

               從此可以看到,我們首先是使用的原始的類的方法,然后分別讓A和B裝飾完以后再調用,最后我們將兩個裝飾器一起使用,再調用該接口定義的方法。

               上述當中,我們分別對待裝飾類進行了原方法的裝飾和新功能的增加,methodA和methodB就是新增加的功能,這些都是裝飾器可以做的,當然兩者並不一定兼有,但一般至少會有一種,否則也就失去了裝飾的意義。

               另外,文章開篇就說道了IO與裝飾器的情緣,相信各位就算不太清楚,也都大致聽說過JAVA的IO是裝飾器模式實現的,所以LZ也不再廢話,在給出一個標准的模板示例以后,直接拿出IO的示例,我們真槍實彈的來。

               下面LZ直接給出IO包中的部分裝飾過程,上面LZ加了詳細的注釋以及各個裝飾器的功能演示,各位可以和上面標准的裝飾器模式對比一下,LZ不得不感嘆,IO與裝飾器的孽緣。

package com.decorator;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PushbackInputStream;
import java.io.PushbackReader;

public class IOTest {

    /* test.txt內容:
     * hello world!
     */
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //文件路徑可自行更換
        final String filePath = "E:/myeclipse project/POITest/src/com/decorator/test.txt";
        
        //InputStream相當於被裝飾的接口或者抽象類,FileInputStream相當於原始的待裝飾的對象,FileInputStream無法裝飾InputStream
        //另外FileInputStream是以只讀方式打開了一個文件,並打開了一個文件的句柄存放在FileDescriptor對象的handle屬性
        //所以下面有關回退和重新標記等操作,都是在堆中建立緩沖區所造成的假象,並不是真正的文件流在回退或者重新標記
        InputStream inputStream = new FileInputStream(filePath);
        final int len = inputStream.available();//記錄一下流的長度
        System.out.println("FileInputStream不支持mark和reset:" + inputStream.markSupported());
        
        System.out.println("---------------------------------------------------------------------------------");
        
        /* 下面分別展示三種裝飾器的作用BufferedInputStream,DataInputStream,PushbackInputStream,LZ下面做了三個裝飾器的功能演示  */
        
        //首先裝飾成BufferedInputStream,它提供我們mark,reset的功能
        BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);//裝飾成 BufferedInputStream
        System.out.println("BufferedInputStream支持mark和reset:" + bufferedInputStream.markSupported());
        bufferedInputStream.mark(0);//標記一下
        char c = (char) bufferedInputStream.read();
        System.out.println("LZ文件的第一個字符:" + c);
        bufferedInputStream.reset();//重置
        c = (char) bufferedInputStream.read();//再讀
        System.out.println("重置以后再讀一個字符,依然會是第一個字符:" + c);
        bufferedInputStream.reset();
        
        System.out.println("---------------------------------------------------------------------------------");
        
        //裝飾成 DataInputStream,我們為了又使用DataInputStream,又使用BufferedInputStream的mark reset功能,所以我們再進行一層包裝
        //注意,這里如果不使用BufferedInputStream,而使用原始的InputStream,read方法返回的結果會是-1,即已經讀取結束
        //因為BufferedInputStream已經將文本的內容讀取完畢,並緩沖到堆上,默認的初始緩沖區大小是8192B
        DataInputStream dataInputStream = new DataInputStream(bufferedInputStream);
        dataInputStream.reset();//這是BufferedInputStream提供的功能,如果不在這個基礎上包裝會出錯
        System.out.println("DataInputStream現在具有readInt,readChar,readUTF等功能");
        int value = dataInputStream.readInt();//讀出來一個int,包含四個字節
        //我們轉換成字符依次顯示出來,可以看到LZ文件的前四個字符
        String binary = Integer.toBinaryString(value);
        int first = binary.length() % 8;
        System.out.print("使用readInt讀取的前四個字符:");
        for (int i = 0; i < 4; i++) {
            if (i == 0) {
                System.out.print(((char)Integer.valueOf(binary.substring(0, first), 2).intValue()));
            }else {
                System.out.print(((char)Integer.valueOf(binary.substring(( i - 1 ) * 8 + first, i * 8 + first), 2).intValue()));
            }
        }
        System.out.println();
        
        System.out.println("---------------------------------------------------------------------------------");
        
        //PushbackInputStream無法包裝BufferedInputStream支持mark reset,因為它覆蓋了reset和mark方法
        //因為流已經被讀取到末尾,所以我們必須重新打開一個文件的句柄,即FileInputStream
        inputStream = new FileInputStream(filePath);
        PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream,len);//裝飾成 PushbackInputStream
        System.out.println("PushbackInputStream裝飾以后支持退回操作unread");
        byte[] bytes = new byte[len];
        pushbackInputStream.read(bytes);//讀完了整個流
        System.out.println("unread回退前的內容:" + new String(bytes));
        pushbackInputStream.unread(bytes);//再退回去
        bytes = new byte[len];//清空byte數組
        pushbackInputStream.read(bytes);//再讀
        System.out.println("unread回退后的內容:" + new String(bytes));
        
        System.out.println("---------------------------------------------------------------------------------");
        
        /*  以上有兩個一層裝飾和一個兩層裝飾,下面我們先裝飾成Reader,再進行其它裝飾   */
        
        //由於之前被PushbackInputStream將流讀取到末尾,我們需要再次重新打開文件句柄
        inputStream = new FileInputStream(filePath);
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"utf-8");//先裝飾成InputStreamReader
        System.out.println("InputStreamReader有reader的功能,比如轉碼:" + inputStreamReader.getEncoding());
        
        System.out.println("---------------------------------------------------------------------------------");
        
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);//我們進一步在reader的基礎上裝飾成BufferedReader
        System.out.println("BufferedReader有readLine等功能:" + bufferedReader.readLine());
        
        System.out.println("---------------------------------------------------------------------------------");
        
        LineNumberReader lineNumberReader = new LineNumberReader(inputStreamReader);//我們進一步在reader的基礎上裝飾成LineNumberReader
        System.out.println("LineNumberReader有設置行號,獲取行號等功能(行號從0開始),當前行號:" + lineNumberReader.getLineNumber());
        
        System.out.println("---------------------------------------------------------------------------------");
        
        //此處由於剛才被readLine方法將流讀取到末尾,所以我們再次重新打開文件句柄,並需要將inputstream再次包裝成reader
        inputStreamReader = new InputStreamReader(new FileInputStream(filePath));
        PushbackReader pushbackReader = new PushbackReader(inputStreamReader,len);//我們進一步在reader的基礎上裝飾成PushbackReader
        System.out.println("PushbackReader是擁有退回操作的reader對象");
        char[] chars = new char[len];
        pushbackReader.read(chars);
        System.out.println("unread回退前的內容:" + new String(chars));
        pushbackReader.unread(chars);//再退回去
        chars = new char[len];//清空char數組
        pushbackReader.read(chars);//再讀
        System.out.println("unread回退后的內容:" + new String(chars));
    }
}

                     上述便是IO的裝飾器使用,其中InputStream就相當於上述的Component接口,只不過這里是一個抽象類,這是我們裝飾的目標抽象類。FileInputstream就是一個ConcreteComponent,即待裝飾的具體對象,它並不是JAVA的IO結構中的一個裝飾器,因為它無法裝飾InputStream。剩下BufferedInputStream,DataInputstream等等就是各種裝飾器了,對比上述的標准裝飾器樣板,JAVA的IO中也有抽象的裝飾器基類的存在,只是上述沒有體現出來,就是FilterInputStream,它是很多裝飾器最基礎的裝飾基類。

                     在上述過程中,其中dataInputStream是經過兩次裝飾后得到的,它具有了dataInputStream和bufferedInputStream的雙重功能,另外,InputStreamReader是一個特殊的裝飾器,它提供了字節流到字符流的橋梁,其實它除了具有裝飾器的特點以外,也有點像一個適配器,但LZ還是覺得它應當算是一個裝飾器。

                    其它的IO裝飾器各位可以自行嘗試或者和上述的標准的裝飾器模式代碼比對一下,下面另附LZ的IO裝飾器程序運行后結果。

                     從上面的展示中,已經可以充分體會到裝飾器模式的靈活了,我們創建的一個FileInputstream對象,我們可以使用各種裝飾器讓它具有不同的特別的功能,這正是動態擴展一個類的功能的最佳體現,而裝飾器模式的靈活性正是JAVA中IO所需要的,不得不贊一下JAVA類庫的建造者實在是強悍。

                     上述的XXXXInputStream的各個類都繼承了InputStream,這樣做不僅是為了復用InputStream的父類功能(InputStream也是一種模板方法模式,它定義了read(byte[])方法的簡單算法,並將read()方法交給具體的InputStream去實現),也是為了可以重疊裝飾,即裝飾器也可以再次被裝飾,而過渡到Reader以后,Reader的裝飾器體系則是類似的。

                     下面LZ給出上面IO包中所涉及的類的類圖,各位可以自行和上面的標准裝飾器模式對比一下。


                     LZ在類圖上標注了各個類負責的角色,並且使用背景顏色將InputStream和Reader體系分開,其中左半部分就是InputStream的裝飾體系,右半部分就是Reader的裝飾體系,並且他們之間的橋梁是InputStreamReader,他們每一個裝飾體系都與上面標准的裝飾器模式類圖極其相似,各位可以自己看一下,感受一下,尤其是InputStreamReader,它的位置比較特殊。

                     總之呢,裝飾器模式就是一個可以非常靈活的動態擴展類功能的設計模式,它采用組合的方式取代繼承,使得各個功能的擴展更加獨立和靈活。

                     本次裝飾器模式就到此結束了,感謝各位的收看,下期再見。

                     下期預告,外觀模式。   

 

 

 


免責聲明!

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



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