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

  最近在總結學習Java I/O相關知識點,I/O應用的場景比較多,不只存在各類I/O源端和想要與之通訊的接收端(文件、控制檯、網絡連接等),並且還須要支持多種不一樣方式的通訊(順序、隨機存取、緩衝、二進制、按字符、按行、按字等)。編程

  Java類庫的設計者經過建立大量的類來解決這個難題,這裏面用到了裝飾器這一設計模式。關於設計模式,以前也有學習過,可是由於比較抽象,加上實際工做中應用較少,因此學習效果每每並非很好,相信大多數人都有這種感受。我以爲學習設計模式仍是須要結合實際應用纔會有更深的理解,而工做中用到各類各樣的設計模式的場景畢竟不是不少,因此結合一些源碼中對設計模式應用的例子來學習我以爲是一種折衷但不失爲效果較好的方式。本文會先總結一下裝飾器這一設計模式,而後結合其在Java I/O類庫設計中的應用來進行學習,相信能夠加深對這一設計模式的理解。設計模式

 

好吃的肉夾饃

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

  好了,本文不是準備講美食的,這只是一個親身經歷,留存在腦海中罷了。由於如今的工做是和編程相關的,因此對於不少生活中的事情我都習慣經過設計將其進行抽象(但願經過學以至用來鍛鍊本身的設計能力,由於這種能力不是一天兩天就能構學好的,須要長期的磨練積累,說遠了。。)。一樣,對於裏脊肉夾饃的價格問題也是能夠抽象成類圖來表示:網絡

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

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

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

  好了,這只是最簡單的模型,咱們經常會有好比加個雞蛋、加根烤腸等等需求,對應肉夾饃的價格也是不同的,這樣怎麼辦呢,咱們能夠直接增長几個子類表明對應的夾饃,這時候類圖就像下面這樣了:flex

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

  看來這種單純經過繼承的解決方案確實存在問題:類爆炸,光寫這些類都是一項很大的開發工做量,並且還要考慮到之後的維護,問題確定會愈來愈多,那怎麼辦?this

   咱們須要做出一些改變:以夾饃爲主體,而後在運行時用材料來「裝飾」肉夾饃。好比說,若是顧客想要裏脊雞蛋肉夾饃,那麼,能夠這樣,先來一個夾饃,以裏脊對象裝飾它,再以雞蛋對象裝飾它,調用cost()方法,裏面會依賴委託將所材料的加錢加上去。這樣,每次有不同的需求,只須要將對應的材料進行裝飾便可,相似以下的步驟:spa

 

  咱們能夠用類圖抽象一下來表示:

  如上圖,咱們定義一個普通夾饃(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類庫的設計來總結了裝飾器模式的相關知識點,但願經過這種方式可以加深你對設計模式的理解,但願能幫到你^_^。

相關文章
相關標籤/搜索