本文參照《Head First 設計模式》,轉載請註明出處 對於整個系列,我們按照這本書的設計邏輯,使用情景分析的方式來描述,並且穿插使用一些問題,總結的方式來講述。並且所有的開發原始碼,都會託管到github上。 專案地址:github.com/jixiang5200…
回顧上一篇文章講解了設計模式中常用的一種模式------觀察者模式。並結合氣象站設計進行實戰解析,並且從自己設計到JAVA自帶設計模式做了講解。想要了解的朋友可以回去回看一下。
本章我們會繼續前面的話題,有關典型的繼承濫用問題。這一章會講解如何使用物件組合的方式,如何在執行時候做裝飾類。在熟悉裝飾技巧後,我們能夠在原本不修改任何底層的程式碼,卻可以給原有物件賦予新的職能。你會說,這不就是“裝飾者模式”。沒錯,接下來就是裝飾者模式的ShowTime時間。
1.前言
歡迎來到星巴茲咖啡,該公司是世界上以擴張速度最快而聞名的咖啡連鎖店。但是最近這家著名的咖啡公司遇到一個巨大的問題,因為擴充套件速度太快了,他們準備更新訂單系統,以合乎他們的飲料供應需求。
他們本來的設計方式如下:
然後客戶購買咖啡時,可以要求在其中加入任何調料,例如:奶茶,牛奶,豆漿。星巴茲根據業務需求會計算相應的費用。這就要求訂單系統必須考慮到這些調料的部分。
然後我們就看到他們的第一個嘗試設計:
是不是有一種犯了密集恐懼症的感覺,整完全就是“類爆炸”。 那麼我們分析一下,這種設計方式違反了什麼設計原則?沒錯,違反了以下兩個原則:
第二設計原則 針對於介面程式設計,不針對實現程式設計
第三設計原則 多用組合,少用繼承
那麼我們應該怎麼修改這個設計呢?
#利用繼承對Beverage類進行改造 首先,我們考慮對基類Beverage類進行修改,我們根據前面“類爆炸”進行分析。主要飲料包含各種調料(牛奶,豆漿,摩卡,奶泡。。。。)。 所以修改後的Beverage類的結構如下:
Beverage類具體實現如下:
public class Beverage {
protected String description;//飲料簡介
protected boolean milk=false;//是否有牛奶
protected boolean soy=false;//是否有豆漿
protected boolean cocha=false;//是否有摩卡
protected boolean whip=false;//是否有奶泡
protected double milkCost=1.01;//牛奶價格
protected double soyCost=1.03;//豆漿價格
protected double cochaCost=2.23;//摩卡價格
protected double whipCost=0.89;//奶泡價格
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean hasMilk() {
return milk;
}
public void setMilk(boolean milk) {
this.milk = milk;
}
public boolean hasSoy() {
return soy;
}
public void setSoy(boolean soy) {
this.soy = soy;
}
public boolean hasCocha() {
return cocha;
}
public void setCocha(boolean cocha) {
this.cocha = cocha;
}
public boolean hasWhip() {
return whip;
}
public void setWhip(boolean whip) {
this.whip = whip;
}
public double getCochaCost() {
return cochaCost;
}
public void setCochaCost(double cochaCost) {
this.cochaCost = cochaCost;
}
public double getWhipCost() {
return whipCost;
}
public void setWhipCost(double whipCost) {
this.whipCost = whipCost;
}
public double cost(){
double condiments=0.0;
if(hasMilk()){//是否需要牛奶
condiments+=milkCost;
}
if(hasSoy()){//是否需要豆漿
condiments+=soyCost;
}
if(hasCocha()){//是否需要摩卡
condiments+=cochaCost;
}
if(hasWhip()){//是否需要奶泡
condiments+=whipCost;
}
return condiments;
}
}
複製程式碼
實現其中一個子類DarkRoast:
public class DarkRoast extends Beverage{
public DarkRoast(){
description="Most Excellent Dark Roast!";
}
public double cost(){
return 1.99+super.cost();
}
}
複製程式碼
看起來很完美,也能滿足現有的業務需求,但是仔細思考一下,真的這樣設計不會出錯?
回答肯定是會出錯。
- 第一,一旦調料的價格發生變化,會導致我們隊原有程式碼進行大改。
- 第二,一旦出現新的調料,我們就需要加上新的方法,並需要改變超類Beverage類中cost()方法。
- 第三,如果星巴茲咖啡研發新的飲料。對於這些飲料而言,某些調料可能並不合適,但是子類仍然會繼承那些本就不合適的方法,例如我就想要一杯水,加奶泡(hasWhip)就不合適。
- 第四,如果使用者需要雙倍的摩卡咖啡,又應該怎麼辦呢?
2.開放-關閉原則
到這裡,我們可以推出最重要的設計原則之一:
第五設計原則 類應該對擴充開放,對修改關閉。
那麼什麼是開放,什麼又是關閉?開放就是允許你使用任何行為來擴充類,如果需求更改(這是無法避免的),就可以進行擴充!關閉在於我們花費很多時間完成開發,並且已經測試釋出,針對後續更改,我們必須關閉原有程式碼防止被修改,避免造成已經測試釋出的原始碼產生新的bug。
綜合上述說法,我們的目標在於允許類擴充,並且在不修改原有程式碼的情況下,就可以搭配新的行為。如果能實現這樣的目標,帶來的好處將相當可觀。在於程式碼會具備彈性來應對需求改變,可以接受增加新的功能用來實現改變的需求。沒錯,這就是擴充開放,修改關閉。
那麼有沒有可以參照的例項可以分析呢?有,就在第二篇我們介紹觀察者模式時,我們介紹到可以通過增加新的觀察者用來擴充主題,並且無需向原主題進行修改。
我們是否需要每個模組都設計成開放--關閉原則?不用,也很難辦到(這樣的人我們稱為“不用設計模式會死病”)。因為想要完全符合開放-關閉原則,會引入大量的抽象層,增加原有程式碼的複雜度。我們應該區分設計中可能改變的部分和不改變的部分(第一設計原則),針對改變部分使用開放--關閉原則。
3.裝飾模式
這裡,就到了開放--關閉原則的運用模式-----裝飾者模式。首先我們還是從星巴茲咖啡的案例來做一個簡單的分析。 分析之前兩個版本(類爆炸和繼承大法)的實現方式,並不能適用於所有的子類。
這就需要一個新的設計思路。這裡,我們將以飲料為主,然後執行的時候以飲料來“裝飾”飲料。舉個栗子,如果影虎需要摩卡和奶泡深焙咖啡,那麼要做的是:
-
拿一個深焙咖啡(DarkRosat)物件
-
以摩卡(Mocha)物件裝飾它
-
以奶泡(Whip)物件裝飾它
-
呼叫cost()方法,並依賴委託(delegate)將調料的價錢加上去。
具體的實現我們用一張圖來展示
-
首先我們構建DarkRoast物件
-
假如顧客需要摩卡(Mocha),再建立一個Mocha物件,並用DarkRoast物件包起來。
-
如果顧客也想要奶泡(Whip),就建立一個Whip裝飾者,並將它用Mocha物件包起來。
-
最後運算客戶的賬單的時候,通過最外層的裝飾者Whip的cost()就可以辦得到。Whip的cost()會委託他的裝飾物件(Mocha)計算出價格,再加上奶泡(Whip)的價格。
通過對星巴茲咖啡的設計方案分析,我們可以發現,所有的裝飾類都具備以下幾個特點:
-
裝飾者和被裝飾物件有相同的超型別。
-
你可以用一個或多個裝飾者包裝一個物件。
-
既然裝飾者和被裝飾物件有相同的超型別,所以在任何需要原始物件(被包裝的)的場合,可以用裝飾過的物件代替它。
-
裝飾者可以在所委託被裝飾者的行為之前與/或之後,加上自己的行為,以達到特定的目的。
-
物件可以在任何時候被裝飾,所以可以在執行時動態地、不限量地用你喜歡的裝飾者來裝飾物件
什麼是裝飾模式呢?我們首先來看看裝飾模式的定義:
裝飾者模式動態地將責任附加到物件上。 若要擴充套件功能,裝飾者提供了比繼承更有彈性 的替代方案。
定義雖然已經定義了裝飾者模式的“角色”,但是未說明怎麼在我們的實現中如何使用它們。我們繼續在星巴茲咖啡中來熟悉相關的操作。
其中裝飾者層級可以無限發展下去,不是如圖中一般兩層關係。並且元件也並非只有一個,可以存在多個。
現在我們就在星巴茲咖啡裡運用裝飾者模式:
到這裡,我們隊裝飾者模式已經有了一個基本的認識。那麼我們已經解決了上面提到的四個問題:
- 第一,一旦調料的價格發生變化,會導致我們隊原有程式碼進行大改。
- 第二,一旦出現新的調料,我們就需要加上新的方法,並需要改變超類Beverage類中cost()方法。
- 第三,如果星巴茲咖啡研發新的飲料。對於這些飲料而言,某些調料可能並不合適,但是子類仍然會繼承那些本就不合適的方法,例如我就想要一杯水,加奶泡(hasWhip)就不合適。
- 第四,如果使用者需要雙倍的摩卡咖啡,又應該怎麼辦呢?
那麼根據第四個問題,假如我們需要雙倍摩卡豆漿奶泡拿鐵咖啡時,該如何去運算賬單呢?首先,我們先把前面的深度烘焙摩卡咖啡的設計圖放在這裡。
然後我們只需要將Mocha的裝飾者加一,即可
4.實現星巴茲咖啡程式碼
前面已經把設計思想都設計出來了,接下來是將其具體實現了。首先從Beverage類下手
public abstract class Beverage1 {
String description="Unknown Beverage";
public String getDescription(){
return description;
}
public abstract double cost();
}
複製程式碼
Beverage類非常簡單,然後再實現Condiment(調料類),該類為抽象類,也為裝飾者類
public abstract class CondimentDecorator extends Beverage1{
//所有的調料裝飾者都必須重新實現 getDescription()方法。
public abstract String getDescription();
}
複製程式碼
前面已經有了飲料的基類,那麼我們來實現一些具體的飲料類。首先從濃縮咖啡(Espresso))開始,這裡需要重寫cost()方法和getDescription()方法
public class Espresso extends Beverage1{
public Espresso(){
//為了要設定飲料的描述,我 們寫了一個構造器。記住, description例項變數繼承自Beverage1
description="Espresso";
}
public double cost() {
//最後,需要計算Espresso的價錢,現在不需要管調料的價錢,直接把Espresso的價格$1.99返回即可。
return 1.99;
}
}
複製程式碼
再實現一個類似的飲料HouseBlend類。
public class HouseBlend extends Beverage1{
public HouseBlend(){
description="HouseBlend";
}
public double cost() {
return 0.89;
}
}
複製程式碼
重新設計DarkRoast1
public class DarkRoast1 extends Beverage1{
public DarkRoast1(){
description="DarkRoast1";
}
public double cost() {
return 0.99;
}
}
複製程式碼
接下來就是調料的程式碼,我們一開始已經實現了抽象元件類(Beverage),有了具體的元件(HouseBlend),也有了已經完成抽象裝飾者(CondimentDecorator)。現在只需要實現具體的裝飾者。首先我們先完成摩卡(Mocha)
public class Mocha extends CondimentDecorator{
/**
* 要讓Mocha能夠引用一個Beverage,採用以下做法
* 1.用一個例項記錄飲料,也就是被裝飾者
* 2.想辦法讓被裝飾者(飲料)被記錄在例項變數中。這裡的做法是:
* 把飲料當作構造器的引數,再由構造器將此飲料記錄在例項變數中
*/
Beverage1 beverage;
public Mocha(Beverage1 beverage) {
this.beverage=beverage;
}
public String getDescription() {
//這裡將調料也體現在相關引數中
return beverage.getDescription()+",Mocha";
}
/**
* 想要計算帶摩卡的飲料的價格,需要呼叫委託給被裝飾者,以計算價格,
* 然後加上Mocha的價格,得到最終的結果。
*/
public double cost() {
return 0.21+beverage.cost();
}
}
複製程式碼
還有奶泡(Whip)類
public class Whip extends CondimentDecorator{
/**
* 要讓Whip能夠引用一個Beverage,採用以下做法
* 1.用一個例項記錄飲料,也就是被裝飾者
* 2.想辦法讓被裝飾者(飲料)被記錄在例項變數中。這裡的做法是:
* 把飲料當作構造器的引數,再由構造器將此飲料記錄在例項變數中
*/
Beverage1 beverage;
public Whip(Beverage1 beverage) {
this.beverage=beverage;
}
public String getDescription() {
//這裡將調料也體現在相關引數中
return beverage.getDescription()+",Whip";
}
/**
* 想要計算帶奶泡的飲料的價格,需要呼叫委託給被裝飾者,以計算價格,
* 然後加上Whip的價格,得到最終的結果。
*/
public double cost() {
return 0.22+beverage.cost();
}
}
複製程式碼
豆漿Soy類
public class Soy extends CondimentDecorator{
/**
* 要讓Soy能夠引用一個Beverage,採用以下做法
* 1.用一個例項記錄飲料,也就是被裝飾者
* 2.想辦法讓被裝飾者(飲料)被記錄在例項變數中。這裡的做法是:
* 把飲料當作構造器的引數,再由構造器將此飲料記錄在例項變數中
*/
Beverage1 beverage;
public Soy(Beverage1 beverage) {
this.beverage=beverage;
}
public String getDescription() {
//這裡將調料也體現在相關引數中
return beverage.getDescription()+",Soy";
}
/**
* 想要計算帶豆漿的飲料的價格,需要呼叫委託給被裝飾者,以計算價格,
* 然後加上Soy的價格,得到最終的結果。
*/
public double cost() {
return 0.21+beverage.cost();
}
}
複製程式碼
接下來就是呼叫測試類,具體實現如下:
public class StarbuzzCoffe {
public static void main(String[] args) {
//訂購一杯Espresso,不需要調料,列印他的價格和描述
Beverage1 beverage=new Espresso();
System.out.println(beverage.getDescription()+"$"
+beverage.cost());
//開始裝飾雙倍摩卡+奶泡咖啡
Beverage1 beverage2=new DarkRoast1();
beverage2=new Mocha(beverage2);
beverage2=new Mocha(beverage2);
beverage2=new Whip(beverage2);
System.out.println(beverage2.getDescription()+"$"
+beverage2.cost());
//
Beverage1 beverage3=new HouseBlend();
beverage3=new Soy(beverage3);
beverage3=new Mocha(beverage3);
beverage3=new Whip(beverage3);
System.out.println(beverage3.getDescription()+"$"
+beverage3.cost());
}
}
複製程式碼
執行結果:
到這裡,我們已經完成裝飾者模式對於星巴茲咖啡的改造。#Java中的真實裝飾者 前面已經研究了裝飾者模式的原理和實現方式,那麼在JAVA語言本身是否有裝飾者模式的使用範例呢,答案是肯定有的,那就是I/O流。
第一次查閱I/O原始碼,都會覺得類真多,而且一環嵌一環,閱讀起來會非常麻煩。但是隻要清楚I/O是根據裝飾者模式設計,就很容易理解。我們先來看一下一個範例:
分析一下,其中BufferedInputStream及LineNumberInputStream都擴充套件自 FilterInputStream,而FilterInputStream是一個抽象的裝飾類。這樣看有些抽象,我們將其中的類按照裝飾者模式進行結構化,方便理解。
我們發現,和星巴茲的設計相比,java.io其實並沒有多大的差異。但是從java.io流我們也會發現裝飾者模式一個非常嚴重的"缺點":使用裝飾者模式,常常會造成設計中有大量的小類,數量還非常多,這對於學習API的程式設計師來說就增加了學習難度和學習成本。但是,懂得裝飾者模式以後會非常容易理解和設計相關的類。
5.設計自己的IO類
在理解裝飾者模式和java.io的設計後,我們將磨鍊下自己的熟悉程度,沒錯,就是自己設計一個Java I/O裝飾者,需求如下:
編寫一個裝飾者,把輸入流內的所有大寫字元轉成小寫。舉例:當讀 取“ ASDFGHJKLQWERTYUIOPZXCVBNM”,裝飾者會將它轉成“ asdghjklqwertyuiopzxcvbnm”。具體的辦法在於擴充套件FilterInputStream類,並覆蓋read()方法就行了。
public class LowerCaseInputStream extends FilterInputStream{
public LowerCaseInputStream(InputStream inputStream){
super(inputStream);
}
public int read() throws IOException {
int c=super.read();
//判斷相關的字元是否為大寫,並轉為小寫
return (c==-1?c:Character.toLowerCase((char)c));
}
/**
*
*針對字元陣列進行大寫轉小寫操作
* @see java.io.FilterInputStream#read(byte[], int, int)
*/
public int read(byte[] b, int off, int len) throws IOException {
int result=super.read(b,off,len);
for(int i=off;i<off+result;i++){
b[i]=(byte) Character.toLowerCase((char)b[i]);
}
return result;
}
}
複製程式碼
接下來我們構建測試類InputTest
public class InputTest {
public static void main(String[] args) {
int c;
try {
InputStream inputStream=new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
while((c=inputStream.read())>=0){
System.out.print((char)c);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
複製程式碼
其中test.txt的內容可以自行編輯,放在專案根目錄下我的內容原文為:
執行結果為:
6.總結
至此,我們已經掌握了裝飾者模式的相關知識點。總結一下:
第五設計原則 類應該對擴充開放,對修改關閉。
裝飾者模式動態地將責任附加到物件上。 若要擴充套件功能,裝飾者提供了比繼承更有彈性 的替代方案。
相應的資料和程式碼託管地址github.com/jixiang5200…