0、背景
來看一個專案需求:咖啡訂購專案。
咖啡種類有很多:美式、摩卡、義大利濃咖啡;
咖啡加料:牛奶、豆漿、可可。
要求是,擴充套件新的咖啡種類的時候,能夠方便維護,不同種類的咖啡需要快速計算多少錢,客戶單點咖啡,也可以咖啡+料。
最差方案
直接想,就是一個咖啡基類,然後所有的單品、所有的組合咖啡都去繼承這個基類,每個類都有自己的對應價格。
問題:那麼多種咖啡和料的組合,都相當於是售賣的咖啡的一個子類,全都去實現基本就是一個全排列,顯然又會類爆炸。並且,擴充套件起來,多一個調料,都要把所有咖啡種類算上重新組合一次。
改進方案
將調料內建到咖啡基類裡,這樣不會造成數量過多,當單品咖啡繼承咖啡基類的時候,就都擁有了這些調料,同時,點沒有點調料,要提供相應的方法,來計算是不是加了這個調料。
問題:這樣的方式雖然改進了類爆炸的問題,但是屬性內建導致了耦合性很強,如果刪了一個調料呢?加了一個調料呢?每一個類都要改,維護量很大。
一、裝飾者模式
裝飾者模式:動態的將新功能附加到物件上,在物件功能擴充套件方面,比繼承更有彈性。
具體實現起來是這樣的,如下類圖所示:
可以看到,在裝飾者裡面擁有一個 Component 物件,這是核心的部分。
也就是不像我們想的,給單品咖啡里加調料,而是反向思維,把單品咖啡拿到調料裡來,決定對他的操作。
如果 ConcreteComponent 很多的話,甚至還可以再增加緩衝層。
用裝飾者模式來解決上面的咖啡訂單問題,類圖設計如下,考慮到具體單品咖啡的種類,增加了一個緩衝層,最基本的抽象類叫 Drink:
其中 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());
}
}
可以看到,呼叫的時候,加佐料只要在原來的 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的子類。
我們一般使用的時候:
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);
//具體操作
感覺真是完全一樣呢。