設計模式學習之:裝飾器模式


  最近在總結學習Java I/O相關知識點,I/O應用的場景比較多,不僅存在各種I/O源端和想要與之通信的接收端(文件、控制台、網絡鏈接等),而且還需要支持多種不同方式的通信(順序、隨機存取、緩沖、二進制、按字符、按行、按字等)。

  Java類庫的設計者通過創建大量的類來解決這個難題,這里面用到了裝飾器這一設計模式。關於設計模式,之前也有學習過,但是因為比較抽象,加上實際工作中應用較少,所以學習效果往往並不是很好,相信大多數人都有這種感覺。我覺得學習設計模式還是需要結合實際應用才會有更深的理解,而工作中用到各種各樣的設計模式的場景畢竟不是很多,所以結合一些源碼中對設計模式應用的例子來學習我覺得是一種折衷但不失為效果較好的方式。本文會先總結一下裝飾器這一設計模式,然后結合其在Java I/O類庫設計中的應用來進行學習,相信可以加深對這一設計模式的理解。

 

好吃的肉夾饃

  首先請原諒一個吃貨用這種方式來講設計模式。上學的時候經常去學校門口的小攤上買里脊肉夾饃,這種食物對於我這種來自南方的同學來說很新奇,所以常常會去買。那時候上學比較節儉,一般都只要里脊(便宜),偶爾會加個雞蛋、烤腸(需要加錢),對這個有些印象,因為攤主每次都是根據顧客定制的需求來算價格的。

  好了,本文不是准備講美食的,這只是一個親身經歷,留存在腦海中罷了。因為現在的工作是和編程相關的,所以對於很多生活中的事情我都習慣通過設計將其進行抽象(希望通過學以致用來鍛煉自己的設計能力,因為這種能力不是一天兩天就能構學好的,需要長期的磨練積累,說遠了。。)。同樣,對於里脊肉夾饃的價格問題也是可以抽象成類圖來表示:

  如上圖,定義一個抽象類ChineseHamburger代表肉夾饃,小攤賣的所有夾饃都需繼承自此類,有兩個方法:

  • getDescription(),抽象方法,可以返回是什么肉夾饃,由子類實現;
  • cost()方法是抽象的,由子類來實現;

  FilletChineseHamburger繼承自ChineseHamburger,代表里脊肉夾饃,實現cost()方法來返回肉夾饃的價格。

  好了,這只是最簡單的模型,我們常常會有比如加個雞蛋、加根烤腸等等需求,對應肉夾饃的價格也是不一樣的,這樣怎么辦呢,我們可以直接增加幾個子類代表對應的夾饃,這時候類圖就像下面這樣了:

  看起來很容易就滿足了需求,但是如果哪天攤主開發一種新的菜品比如雞柳、生菜,或者我們既想加雞蛋又想加里脊呢?按照這種方式我們是不是需要提供很多子類來實現各自的計價,想象一下與日俱增的子類數量,你是不是要崩潰了?

  看來這種單純通過繼承的解決方案確實存在問題:類爆炸,光寫這些類都是一項很大的開發工作量,而且還要考慮到以后的維護,問題肯定會越來越多,那怎么辦?

   我們需要作出一些改變:以夾饃為主體,然后在運行時用材料來“裝飾”肉夾饃。比如說,如果顧客想要里脊雞蛋肉夾饃,那么,可以這樣,先來一個夾饃,以里脊對象裝飾它,再以雞蛋對象裝飾它,調用cost()方法,里面會依賴委托將所材料的加錢加上去。這樣,每次有不一樣的需求,只需要將對應的材料進行裝飾即可,類似如下的步驟:

 

  我們可以用類圖抽象一下來表示:

  如上圖,我們定義一個普通夾饃(SimpleChineseHamburger)。再定義一個裝飾器Decorator,其包含一個夾饃對象,並可以對其進行“裝飾”(就是委托其進行計價),這樣一來我們每多加一種材料,只需要多裝飾一次即可,避免了重復設計大量的相似類。這,就是裝飾器模式的應用。

 

什么是裝飾器模式

   裝飾器模式的說明:動態地將責任附加到對象上。若要擴展功能,裝飾者提供了比繼承更有彈性的替代方案。原文是:

Attach additional responsibilities to an object dynamically keeping the same interface.Decorators provide a flexible alternative to subclassing for extending functionality.

  我們來看一下類圖:

  在類圖中,各個角色的說明如下:

Component,抽象構件

  Component是一個接口或者抽象類,是定義我們最核心的對象,也可以說是最原始的對象,比如上面的肉夾饃。

ConcreteComponent,具體構件,或者基礎構件

  ConcreteComponent是最核心、最原始、最基本的接口或抽象類Component的實現,可以單獨用,也可將其進行裝飾,比如上面的簡單肉夾饃。

Decorator,裝飾角色

  一般是一個抽象類,繼承自或實現Component,在它的屬性里面有一個變量指向Component抽象構件,我覺得這是裝飾器最關鍵的地方。

ConcreteDecorator,具體裝飾角色

  ConcreteDecoratorA和ConcreteDecoratorB是兩個具體的裝飾類,它們可以把基礎構件裝飾成新的東西,比如把一個普通肉夾饃裝飾成雞蛋里脊肉夾饃。

  光解釋比較抽象,我們再來看看代碼實現,先看抽象構件:

public abstract class Component{
    // 抽象地方法
    public abstract void cost();
}

  然后是具體基礎構件:

public class ConcreteComponent extends Component{
    @Override
    public void cost(){
        // do something ...
    }
}

  抽象裝飾角色:

public abstract class Decorator extends Component{
    private Component component = null;
    public Decorator(Component component){
        this.component = component;
    }
    @Override
    public void cost(){
        this.component.cost();
    }
}

  具體裝飾角色:

public class ConcreteDecorator extends Decorator{
    public ConcreteDecorator(Component component){
        super(component);
    }

    // 定義自己的修飾邏輯
    private void decorateMethod(){
        // do somethind ... 
    }

    // 重寫父類的方法
    public void cost(){
        this.decorateMethod();
        super.cost();
    }
}

   我們可以通過一個具體例子來看一下裝飾器模式是如何運行的:

public class DecoratorDemo{
    public static void main(String[] args){
        Component component = new ConcreteComponent();
        // 第一次修飾,比如,加雞蛋,加1塊
        component = new ConcreteDecorator(component);
        // 第二次修飾,比如,加烤腸,加2塊
        component = new ConcreteDecorator(component);
        // 修飾后運行,將錢加在一起
        component.cost();
    }
}

 

裝飾器模式在Java I/O系統中的實現

   前面總結了這么多,再從大神們的作品中找一個實際應用例子吧,畢竟那是經歷實戰檢驗的,肯定是有道理的。嗯,在平時的留意中我發現Java I/O系統的設計中用到了這一設計模式,因為Java I/O類庫需要多種不同功能的組合。這里我就以InputStream為例簡單說明一下,同樣我們還是來看一下其類圖:

  InputStream作為抽象構件,其下面大約有如下幾種具體基礎構件,從不同的數據源產生輸入:

  • ByteArrayInputStream,從字節數組產生輸入;
  • FileInputStream,從文件產生輸入;
  • StringBufferInputStream,從String對象產生輸入;
  • PipedInputStream,從管道產生輸入;
  • SequenceInputStream,可將其他流收集合並到一個流內;

  FilterInputStream作為裝飾器在JDK中是一個普通類,其下面有多個具體裝飾器比如BufferedInputStream、DataInputStream等。我們以BufferedInputStream為例,使用它就是避免每次讀取時都進行實際的寫操作,起着緩沖作用。我們可以在這里稍微深入一下,站在源碼的角度來管中窺豹。

  FilterInputStream內部封裝了基礎構件:

protected volatile InputStream in;

  而BufferedInputStream在調用其read()讀取數據時會委托基礎構件來進行更底層的操作,而它自己所起的裝飾作用就是緩沖,在源碼中可以很清楚的看到這一切:

    public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }


    private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();
        if (markpos < 0)
            pos = 0;            /* no mark: throw away the buffer */
        else if (pos >= buffer.length)  /* no room left in buffer */
            if (markpos > 0) {  /* can throw away early part of the buffer */
                int sz = pos - markpos;
                System.arraycopy(buffer, markpos, buffer, 0, sz);
                pos = sz;
                markpos = 0;
            } else if (buffer.length >= marklimit) {
                markpos = -1;   /* buffer got too big, invalidate mark */
                pos = 0;        /* drop buffer contents */
            } else if (buffer.length >= MAX_BUFFER_SIZE) {
                throw new OutOfMemoryError("Required array size too large");
            } else {            /* grow buffer */
                int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
                        pos * 2 : MAX_BUFFER_SIZE;
                if (nsz > marklimit)
                    nsz = marklimit;
                byte nbuf[] = new byte[nsz];
                System.arraycopy(buffer, 0, nbuf, 0, pos);
                if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                    throw new IOException("Stream closed");
                }
                buffer = nbuf;
            }
        count = pos;
        // 看這行就行了,委托基礎構件來進行更底層的操作
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
        if (n > 0)
            count = n + pos;
    }

    private InputStream getInIfOpen() throws IOException {
        InputStream input = in;
        if (input == null)
            throw new IOException("Stream closed");
        return input;
    }

  這部分的代碼很多,這里我們沒有必要考慮這段代碼的具體邏輯,只需要看到在BufferedInputStream的read方法中通過getInIfOpen()獲取基礎構件從而委托其進行更底層的操作(在這里是讀取單個字節)就可以說明本文所要說的一切了。

  至於I/O類庫中的其他設計諸如OutputStream、Writer、Reader,是一致的,這里就不再贅述了。

 

總結

  本文介紹了裝飾器模式,其有如下優點:

  • 裝飾類和被裝飾類可以獨立發展,而不會相互耦合。換句話說,Component類無需知道Decorator類,Decorator類是從外部來擴展Component類的功能,而Decorator也不用知道具體的構件。
  • 裝飾器模式是繼承關系的一個替代方案。我們看裝飾類Decorator,不管裝飾多少層,返回的對象還是Component(因為Decorator本身就是繼承自Component的),實現的還是is-a的關系。
  • 裝飾模式可以動態地擴展一個實現類的功能,比如在I/O系統中,我們直接給BufferedInputStream的構造器直接傳一個InputStream就可以輕松構件一個帶緩沖的輸入流,如果需要擴展,我們繼續“裝飾”即可。

  但是也有其自身的缺點:

  多層的裝飾是比較復雜的。為什么會復雜?你想想看,就像剝洋蔥一樣,你剝到最后才發現是最里層的裝飾出現了問題,可以想象一下工作量。這點從我使用Java I/O的類庫就深有感受,我只需要單一結果的流,結果卻往往需要創建多個對象,一層套一層,對於初學者來說容易讓人迷惑。

  本文結合Java I/O類庫的設計來總結了裝飾器模式的相關知識點,希望通過這種方式能夠加深你對設計模式的理解,希望能幫到你^_^。

 


免責聲明!

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



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