設計模式:裝飾者模式介紹及程式碼示例 && JDK裡關於裝飾者模式的應用

Life_Goes_On發表於2020-08-16

0、背景

來看一個專案需求:咖啡訂購專案。

咖啡種類有很多:美式、摩卡、義大利濃咖啡;
咖啡加料:牛奶、豆漿、可可。

要求是,擴充套件新的咖啡種類的時候,能夠方便維護,不同種類的咖啡需要快速計算多少錢,客戶單點咖啡,也可以咖啡+料。

最差方案

直接想,就是一個咖啡基類,然後所有的單品、所有的組合咖啡都去繼承這個基類,每個類都有自己的對應價格。

問題:那麼多種咖啡和料的組合,都相當於是售賣的咖啡的一個子類,全都去實現基本就是一個全排列,顯然又會類爆炸。並且,擴充套件起來,多一個調料,都要把所有咖啡種類算上重新組合一次。

改進方案

將調料內建到咖啡基類裡,這樣不會造成數量過多,當單品咖啡繼承咖啡基類的時候,就都擁有了這些調料,同時,點沒有點調料,要提供相應的方法,來計算是不是加了這個調料。

問題:這樣的方式雖然改進了類爆炸的問題,但是屬性內建導致了耦合性很強,如果刪了一個調料呢?加了一個調料呢?每一個類都要改,維護量很大。

一、裝飾者模式

裝飾者模式動態的將新功能附加到物件上,在物件功能擴充套件方面,比繼承更有彈性。

具體實現起來是這樣的,如下類圖所示:

設計模式:裝飾者模式介紹及程式碼示例 && JDK裡關於裝飾者模式的應用

可以看到,在裝飾者裡面擁有一個 Component 物件,這是核心的部分。

也就是不像我們想的,給單品咖啡里加調料,而是反向思維,把單品咖啡拿到調料裡來,決定對他的操作。

如果 ConcreteComponent 很多的話,甚至還可以再增加緩衝層。

用裝飾者模式來解決上面的咖啡訂單問題,類圖設計如下,考慮到具體單品咖啡的種類,增加了一個緩衝層,最基本的抽象類叫 Drink

設計模式:裝飾者模式介紹及程式碼示例 && JDK裡關於裝飾者模式的應用

其中 Drink 就相當於是前面的 Component,Coffee 是緩衝層,下面的不同 Coffee 就是上面的ConcreteConponent。

費用的計算方式一改正常思路的咖啡中,而是在調料中,因為 cost 在 Drink 類裡也有,所以到最終的計算,其實是帶上之前的 cost 結果,如果多種裝飾者進行裝飾,比如一個coffee加了很多料,那麼其實是 遞迴的思路計算 最後的 cost 。

這樣的話,增加一個單品咖啡,或者增加調料,都不用改變其他地方。

程式碼如下,類比較多,但是每個都比較簡單:

/*
    抽象類Drink,相當於Component;
    getset方法提供給子類去設定飲品或調料的資訊
    但是:cost方法留給調料部分實現
*/
public abstract class Drink {
    public String description;
    private float price = 0.0f;
    //價格方法
    public abstract float cost();
    public float getPrice() {
        return price;
    }
    public void setPrice(float price) {
        this.price = price;
    }
    public String getDescription() {
        return description +":"+ price;
    }
    public void setDescription(String description) {
        this.description = description;
    }
}

接著就是Coffe緩衝層以及下面的實現類,相當於ConcreteComponent:

public class Coffee extends Drink{
    @Override
    public float cost() {
        return super.getPrice();
    }
}
public class MochaCoffee extends Coffee{
    public MochaCoffee() {
        setDescription(" 摩卡咖啡 ");
        setPrice(7.0f);
    }
}
public class USCoffee extends Coffee{
    public USCoffee() {
        setDescription(" 美式咖啡 ");
        setPrice(5.0f);
    }
}
public class ItalianCoffee extends Coffee {
    public ItalianCoffee(){
        setDescription(" 義大利咖啡 ");
        setPrice(6.0f);
    }
}

然後是裝飾核心,Decorator,和Drink是繼承+組合的關係:

/*
    Decorator,反客為主去拿已經有price的drink,並加上佐料
    加佐料的時候是拿去了Drink物件,但是也是給Drink進行
*/
public class Decorator extends Drink{
    private Drink drink;
    //提供一個構造器
    public Decorator(Drink drink){
        this.drink = drink;
    }
    @Override
    public float cost() {
        //計算成本,拿到佐料自己的價格+本來一杯Drink的價格
        //這裡注意呼叫的是drink.cost不是drink.getPrice,因為cost才是子類實現的,Drink類的getPrice方法預設是返回0
        return super.getPrice() + drink.cost();
    }

    @Override
    public String getDescription() {
        //自己的資訊+被裝飾者coffee的資訊
        return description + " " + getPrice() + " &&" + drink.getDescription();
    }
}

以及Decorator的實現類,也就是ConcreteDecorator:

public class Milk extends Decorator{
    public Milk(Drink drink) {
        super(drink);
        setDescription(" 牛奶:");
        setPrice(1.0f);
    }
}
public class Coco extends Decorator{
    public Coco(Drink drink) {
        super(drink);
        setDescription(" 可可:");
        setPrice(2.0f);//調味品價格
    }
}
public class Sugar extends Decorator {
    public Sugar(Drink drink) {
        super(drink);
        setDescription(" 糖:");
        setPrice(0.5f);
    }
}

注意,對於具體的Decorator,這裡就體現了逆向思維,拿到的 drink 物件,呼叫父類構造器得到了一個drink,然後 set 和 get 方法設定調料自己的price和description,父類的方法 cost 就會計算價錢綜合。

那裡面的 super.getPrice() + drink.cost() 中的 cost(),就是一個遞迴的過程。

最後我們來寫一個客戶端測試:

public class Client {
    public static void main(String[] args) {
        //1.點一個咖啡,用Drink接受,因為還沒有完成裝飾
        Drink usCoffee = new USCoffee();
        System.out.println("費用:"+usCoffee.cost()+" 飲品資訊:"+usCoffee.getDescription());
        //2.加料
        usCoffee = new Milk(usCoffee);
        System.out.println("加奶後:"+usCoffee.cost()+" 飲品資訊:"+usCoffee.getDescription());
        //3.再加可可
        usCoffee = new Coco(usCoffee);
        System.out.println("加奶和巧克力後:"+usCoffee.cost()+" 飲品資訊:"+usCoffee.getDescription());
    }
}
設計模式:裝飾者模式介紹及程式碼示例 && JDK裡關於裝飾者模式的應用

可以看到,呼叫的時候,加佐料只要在原來的 drink 物件的基礎上,重新構造,將原來的 drink 放進去包裝(裝飾),最後就達到了效果。

並且,如果要擴充套件一個型別的 coffee 或者一個調料,只用增加自己一個類就可以。

二、裝飾者模式在 JDK 裡的應用

java 的 IO 結構,FilterInputStream 就是一個裝飾者。

設計模式:裝飾者模式介紹及程式碼示例 && JDK裡關於裝飾者模式的應用

2.1 這裡面 InputStream 就相當於 Drink,也就是 Component 部分;
2.2 FileInputStream、StringBufferInputStream、ByteArrayInputStream 就相當於是單品咖啡,也就是ConcreteComponent,是 InputStream 的子類;
2.3 而 FilterInputStream 就相當於 Decorator,繼承 InputStream 的同時又組合了InputStream;

設計模式:裝飾者模式介紹及程式碼示例 && JDK裡關於裝飾者模式的應用

2.4 BufferInputStream、DataInputStream、LineNumberInputStream 相當於具體的調料,是FilterInputStream的子類。

我們一般使用的時候:

DataInputStream dataInputStream = new DataInputStream(new FileInputStream("D://test.txt"));

或者:

FileInputStream fi = new FileInputStream("D:\\test.txt");
DataInputStream dataInputStream = new DataInputStream(fi);
//具體操作

這裡面的 fi 就相當於單品咖啡, datainputStream 就是給他加了佐料。

更貼合上面咖啡的寫法,宣告的時候用 InputStream 接他,就可以:

InputStream fi = new FileInputStream("D:\\test.txt");
fi = new DataInputStream(fi);
//具體操作

感覺真是完全一樣呢。

相關文章