1. 程式人生 > 程式設計 >Java設計模式-裝飾者模式

Java設計模式-裝飾者模式

著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。

定義

裝飾者模式:在不改變原類檔案以及不使用繼承的情況下,動態地將責任附加到物件上,從而實現動態拓展一個物件的功能。它是通過建立一個包裝物件,也就是裝飾來包裹真實的物件。

原則

要使用裝飾者模式,需要滿足以下設計原則:

  1. 多用組合,少用繼承
  2. 開放-關閉原則:類應該對拓展開放,對修改關閉

UML類圖

我們先來看看裝飾者模式的類圖,再來詳細講述:

由上自下:

  1. Component是基類。通常是一個抽象類或者一個介面,定義了屬性或者方法,方法的實現可以由子類實現或者自己實現。通常不會直接使用該類,而是通過繼承該類來實現特定的功能,它約束了整個繼承樹的行為。比如說,如果Component
    代表人,即使通過裝飾也不會使人變成別的動物。
  2. ConcreteComponenComponent的子類,實現了相應的方法,它充當了“被裝飾者”的角色。
  3. Decorator也是Component的子類,它是裝飾者共同實現的抽象類(也可以是介面)。比如說,Decorator代表衣服這一類裝飾者,那麼它的子類應該是T恤、裙子這樣的具體的裝飾者。
  4. ConcreteDecoratorDecorator的子類,是具體的裝飾者,由於它同時也是Component的子類,因此它能方便地拓展Component的狀態(比如新增新的方法)。每個裝飾者都應該有一個例項變數用以儲存某個Component的引用,這也是利用了組合的特性。在持有Component的引用後,由於其自身也是Component
    的子類,那麼,相當於ConcreteDecorator包裹了Component,不但有Component的特性,同時自身也可以有別的特性,也就是所謂的裝飾。

例項演示

為了更加深刻地理解裝飾者模式,我們來看一個簡單的栗子。首先,我們假設現在有這樣一個需求:你有一家服裝店,賣各式各樣的衣服,現在需要用一個系統來記錄客戶所要購買的衣服的總價,以便方便地結算。那麼在這個例子裡面,我們可以用裝飾者模式,把客戶當做被裝飾者,衣服是裝飾者,這很直觀形象吧,接著我們來一步步實現需求。

建立Component基類

因為總體物件是人,所以我們可以把人抽象為基類,新建Person.java:

public abstract class Person {
    String description = "Unkonwn"
; public String getDescription() { return description; } public abstract double cost(); //子類應該實現的方法 } 複製程式碼

建立被裝飾者——ConcreteComponent

客戶分為很多種,有兒童、青少年、成年人等,因此我們可以建立不同的被裝飾者,這裡我們建立青少年的被裝飾者,新建Teenager.java:

public class Teenager extends Person {
    
    public Teenager() {
        this.description = "Shopping List:";
    }
    
    @Override
    public double cost() {
        //什麼都沒買,不用錢
        return 0;
    }

}
複製程式碼

建立Decorator

由於不同的部位有不同的衣物,不能混為一談,比如說,衣服、帽子、鞋子等,那麼這裡我們建立的Decorator為衣服和帽子,分別新建ClothingDecorator.javaHatDecorator.java:

public abstract class ClothingDecorator extends Person {
    
    public abstract String getDescription();
}
複製程式碼
public abstract class HatDecorator extends Person {

    public abstract String getDescription();

}
複製程式碼

建立ConcreteDecorator

上面既然已經建立了兩種Decorator,那麼我們基於它們進行拓展,創建出不同的裝飾者,對於Clothing,我們新建Shirt.java,對於Hat,我們新建Casquette,其實可以根據不同型別的衣物建立更多不同的裝飾者,這裡只是作為演示而建立了兩種。程式碼如下所示:

public class Shirt extends ClothingDecorator {
    
    //用例項變數儲存Person的引用
    Person person;
    
    public Shirt(Person person)
    {
        this.person = person;
    }
    
    @Override
    public String getDescription() {
        return person.getDescription() + "a shirt  ";
    }

    @Override
    public double cost() {
        return 100 + person.cost(); //實現了cost()方法,並呼叫了person的cost()方法,目的是獲得所有累加值
    }

}
複製程式碼
public class Casquette extends HatDecorator {
    
    Person person;
    
    public Casquette(Person person) {
        this.person = person;
    }
    @Override
    public String getDescription() {
        return person.getDescription() + "a casquette  "; //鴨舌帽
    }

    @Override
    public double cost() {
        return 75 + person.cost();
    }

}
複製程式碼

最後我們在測試類測試我們的程式碼:

public class Shopping {

    public static void main(String[] args) {
        Person person = new Teenager();
        
        person = new Shirt(person);
        person = new Casquette(person);
        
        System.out.println(person.getDescription() + " ¥ " +person.cost());
    }

}
複製程式碼

先建立一個Teenager物件,接著用Shirt裝飾它,就變成了穿著ShirtTeenager,再用Casquette裝飾,就變成了戴著Casquette的穿著ShirtTeenager。執行結果如下所示:

我們梳理一下以上的邏輯,畫出如下所示的韋恩圖:

Teenager、Shirt、Casquette都是繼承自Person基類,但是具體實現不同,TeenagerPerson的直接子類,表示了被裝飾者;Teenager、Shirt是裝飾者,儲存了Person的引用,實現了cost()方法,並且在cost()方法內部,不但實現了自己的邏輯,同時也呼叫了Person引用的cost()方法,即獲取了被裝飾者的資訊,這是裝飾者的一個特點,儲存引用的目的就是為了獲取被裝飾者的狀態資訊,以便將自身的特性加以組合。

總結

以上就是裝飾者模式的一個小栗子,講述了裝飾者的基本用法。總結一下裝飾者模式的特點。

  1. 裝飾者和被裝飾者有相同的介面(或有相同的父類)。
  2. 裝飾者儲存了一個被裝飾者的引用。
  3. 裝飾者接受所有客戶端的請求,並且這些請求最終都會返回給被裝飾者(參見韋恩圖)。
  4. 在執行時動態地為物件新增屬性,不必改變物件的結構。

使用裝飾者模式的最大好處就是其拓展性十分良好,通過使用不同的裝飾類來使得物件具有多種多樣的屬性,靈活性比直接繼承好。然而它也有缺點,那就是會出現很多小類,即裝飾類,使程式變得複雜。

擴充套件閱讀

學習了裝飾者模式用法、特點以及優缺點後,我們再來看看裝飾者模式在實際開發過程的應用。裝飾者模式在Java中經常出現的地方就是JavaIO。提到JavaIO,腦海中就冒出了大量的類:InputStream、FileInputStream、BufferedInputStream……等,真是頭都大了,其實,這裡面大部分都是裝飾類,只要弄清楚這一點就容易理解了。我們來看看JavaIO是怎樣使用裝飾者模式的。

從字元流來分析,我們知道,有兩個基類,分別是InputStreamOutputStream,它們也就是我們上面所述的Component基類。接著,它有如下子類:FileInputStream、StringBufferInputStream等,它們就代表了上面所述的ConcreteComponent,即裝飾物件。此外,InputStream還有FilterInputStream這個子類,它就是一個抽象裝飾者,即Decorator,那麼它的子類:BufferedInputStream、DataInputStream等就是具體的裝飾者了。那麼,從裝飾者模式的角度來看JavaIO,是不是更加容易理解了呢?

下面,我們來自己實現自己的JavaIO的裝飾者。要實現的功能是:把一段話裡面的每個單詞的首字母大寫。

我們先新建一個類:UpperFirstWordInputStream.java

public class UpperFirstWordInputStream extends FilterInputStream {
    
    private int cBefore = 32;
    
    protected UpperFirstWordInputStream(InputStream in) {
        //由於FilterInputStream已經儲存了裝飾物件的引用,這裡直接呼叫super即可
        super(in);
    }
    
    public int read() throws IOException{
        //根據前一個字元是否是空格來判斷是否要大寫
        int c = super.read();
        if(cBefore == 32)
        {
            cBefore = c;
            return (c == -1 ? c: Character.toUpperCase((char) c));
        }else{
            cBefore = c;
            return c;
        }
            
    }
    
}
複製程式碼

接著編寫一個測試類:InputTest.java

public class InputTest {

    public static void main(String[] args) throws IOException {
        int c;
        StringBuffer sb = new StringBuffer();
        try {
            //這裡用了兩個裝飾者,分別是BufferedInputStream和我們的UpperFirstWordInputStream
            InputStream in = new UpperFirstWordInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
            while((c = in.read()) >= 0)
            {
                sb.append((char) c);
            }
            System.out.println(sb);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

(注意:上面的test.txt檔案需要你自行建立,放到同一個資料夾內即可,內容可隨意填寫。) 最後,我們看下執行結果:

掃碼關注更多內容