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

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


# 0、背景
來看一個專案需求:咖啡訂購專案。 咖啡種類有很多:美式、摩卡、義大利濃咖啡; 咖啡加料:牛奶、豆漿、可可。 要求是,擴充套件新的咖啡種類的時候,能夠方便維護,不同種類的咖啡需要快速**計算多少錢**,客戶單點咖啡,也可以咖啡+料。 ### 最差方案 直接想,就是一個咖啡基類,然後所有的單品、所有的組合咖啡都去繼承這個基類,每個類都有自己的對應價格。 **問題**:那麼多種咖啡和料的組合,都相當於是售賣的咖啡的一個子類,全都去實現基本就是一個全排列,顯然又會類爆炸。並且,擴充套件起來,多一個調料,都要把所有咖啡種類算上重新組合一次。 ### 改進方案 將調料內建到咖啡基類裡,這樣不會造成數量過多,當單品咖啡繼承咖啡基類的時候,就都擁有了這些調料,同時,點沒有點調料,要提供相應的方法,來計算是不是加了這個調料。 **問題**:這樣的方式雖然改進了類爆炸的問題,但是屬性內建導致了耦合性很強,如果刪了一個調料呢?加了一個調料呢?每一個類都要改,維護量很大。
# 一、裝飾者模式
**裝飾者模式**:動態的將新功能附加到物件上,在物件功能擴充套件方面,比繼承更有彈性。 具體實現起來是這樣的,如下類圖所示:
可以看到,在裝飾者裡面擁有一個 Component 物件,這是核心的部分。 也就是不像我們想的,給單品咖啡里加調料,而是**反向思維,把單品咖啡拿到調料裡來,決定對他的操作。** 如果 ConcreteComponent 很多的話,甚至還可以再增加緩衝層。 用裝飾者模式來解決上面的咖啡訂單問題,類圖設計如下,考慮到具體單品咖啡的種類,增加了一個緩衝層,**最基本的抽象類叫 Drink**:
**其中 Drink 就相當於是前面的 Component,Coffee 是緩衝層,下面的不同 Coffee 就是上面的ConcreteConponent。** 費用的計算方式一改正常思路的咖啡中,而**是在調料中**,因為 cost 在 Drink 類裡也有,所以到最終的計算,其實是帶上之前的 cost 結果,如果多種裝飾者進行裝飾,比如一個coffee加了很多料,那麼其實是 **遞迴的思路計算** 最後的 cost 。 這樣的話,增加一個單品咖啡,或者增加調料,都不用改變其他地方。 程式碼如下,類比較多,但是每個都比較簡單: ```java /* 抽象類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: ```java public class Coffee extends Drink{ @Override public float cost() { return super.getPrice(); } } ``` ```java public class MochaCoffee extends Coffee{ public MochaCoffee() { setDescription(" 摩卡咖啡 "); setPrice(7.0f); } } ``` ```java public class USCoffee extends Coffee{ public USCoffee() { setDescription(" 美式咖啡 "); setPrice(5.0f); } } ``` ```java public class ItalianCoffee extends Coffee { public ItalianCoffee(){ setDescription(" 義大利咖啡 "); setPrice(6.0f); } } ``` 然後是裝飾核心,Decorator,和Drink是繼承+組合的關係: ```java /* 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: ```java public class Milk extends Decorator{ public Milk(Drink drink) { super(drink); setDescription(" 牛奶:"); setPrice(1.0f); } } ``` ```java public class Coco extends Decorator{ public Coco(Drink drink) { super(drink); setDescription(" 可可:"); setPrice(2.0f);//調味品價格 } } ``` ```java 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(),就是一個**遞迴**的過程。 最後我們來寫一個客戶端測試: ```java 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()); } } ```
可以看到,呼叫的時候,加佐料只要在原來的 drink 物件的基礎上,重新構造,將原來的 drink 放進去包裝(裝飾),最後就達到了效果。 並且,如果要**擴充套件**一個型別的 coffee 或者一個調料,只用增加自己一個類就可以。
# 二、裝飾者模式在 JDK 裡的應用
java 的 IO 結構,FilterInputStream 就是一個裝飾者。
**2.1 這裡面 InputStream 就相當於 Drink,也就是 Component 部分; 2.2 FileInputStream、StringBufferInputStream、ByteArrayInputStream 就相當於是單品咖啡,也就是ConcreteComponent,是 InputStream 的子類; 2.3 而 FilterInputStream 就相當於 Decorator,繼承 InputStream 的同時又組合了InputStream;**
**2.4 BufferInputStream、DataInputStream、LineNumberInputStream 相當於具體的調料,是FilterInputStream的子類。** 我們一般使用的時候: ```java DataInputStream dataInputStream = new DataInputStream(new FileInputStream("D://test.txt")); ``` 或者: ```java FileInputStream fi = new FileInputStream("D:\\test.txt"); DataInputStream dataInputStream = new DataInputStream(fi); //具體操作 ``` 這裡面的 fi 就相當於單品咖啡, datainputStream 就是給他加了佐料。 更貼合上面咖啡的寫法,宣告的時候用 InputStream 接他,就可以: ```java InputStream fi = new FileInputStream("D:\\test.txt"); fi = new DataInputStream(fi); //具體操作 ``` 感覺真是完全一