本文參照《Head First 設計模式》,轉載請註明出處 對於整個系列,咱們按照這本書的設計邏輯,使用情景分析的方式來描述,而且穿插使用一些問題,總結的方式來說述。而且全部的開發源碼,都會託管到github上。 項目地址:github.com/jixiang5200…java
回顧上一篇文章講解了設計模式中經常使用的一種模式------觀察者模式。並結合氣象站設計進行實戰解析,而且從本身設計到JAVA自帶設計模式作了講解。想要了解的朋友能夠回去回看一下。git
本章咱們會繼續前面的話題,有關典型的繼承濫用問題。這一章會講解如何使用對象組合的方式,如何在運行時候作裝飾類。在熟悉裝飾技巧後,咱們可以在本來不修改任何底層的代碼,卻能夠給原有對象賦予新的職能。你會說,這不就是「裝飾者模式」。沒錯,接下來就是裝飾者模式的ShowTime時間。程序員
歡迎來到星巴茲咖啡,該公司是世界上以擴張速度最快而聞名的咖啡連鎖店。可是最近這家著名的咖啡公司遇到一個巨大的問題,由於擴展速度太快了,他們準備更新訂單系統,以合乎他們的飲料供應需求。github
他們原本的設計方式以下: 編程
而後客戶購買咖啡時,能夠要求在其中加入任何調料,例如:奶茶,牛奶,豆漿。星巴茲根據業務需求會計算相應的費用。這就要求訂單系統必須考慮到這些調料的部分。設計模式
而後咱們就看到他們的第一個嘗試設計:數組
是否是有一種犯了密集恐懼症的感受,整徹底就是「類爆炸」。 那麼咱們分析一下,這種設計方式違反了什麼設計原則?沒錯,違反了如下兩個原則:bash
第二設計原則 針對於接口編程,不針對實現編程學習
第三設計原則 多用組合,少用繼承測試
那麼咱們應該怎麼修改這個設計呢?
#利用繼承對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();
}
}
複製代碼
看起來很完美,也能知足現有的業務需求,可是仔細思考一下,真的這樣設計不會出錯?
回答確定是會出錯。
到這裏,咱們能夠推出最重要的設計原則之一:
第五設計原則 類應該對拓展開放,對修改關閉。
那麼什麼是開放,什麼又是關閉?開放就是容許你使用任何行爲來拓展類,若是需求更改(這是沒法避免的),就能夠進行拓展!關閉在於咱們花費不少時間完成開發,而且已經測試發佈,針對後續更改,咱們必須關閉原有代碼防止被修改,避免形成已經測試發佈的源碼產生新的bug。
綜合上述說法,咱們的目標在於容許類拓展,而且在不修改原有代碼的狀況下,就能夠搭配新的行爲。若是能實現這樣的目標,帶來的好處將至關可觀。在於代碼會具有彈性來應對需求改變,能夠接受增長新的功能用來實現改變的需求。沒錯,這就是拓展開放,修改關閉。
那麼有沒有能夠參照的實例能夠分析呢?有,就在第二篇咱們介紹觀察者模式時,咱們介紹到能夠經過增長新的觀察者用來拓展主題,而且無需向原主題進行修改。
咱們是否須要每一個模塊都設計成開放--關閉原則?不用,也很難辦到(這樣的人咱們稱爲「不用設計模式會死病」)。由於想要徹底符合開放-關閉原則,會引入大量的抽象層,增長原有代碼的複雜度。咱們應該區分設計中可能改變的部分和不改變的部分(第一設計原則),針對改變部分使用開放--關閉原則。
這裏,就到了開放--關閉原則的運用模式-----裝飾者模式。首先咱們仍是從星巴茲咖啡的案例來作一個簡單的分析。 分析以前兩個版本(類爆炸和繼承大法)的實現方式,並不能適用於全部的子類。
這就須要一個新的設計思路。這裏,咱們將以飲料爲主,而後運行的時候以飲料來「裝飾」飲料。舉個栗子,若是影虎鬚要摩卡和奶泡深焙咖啡,那麼要作的是:
拿一個深焙咖啡(DarkRosat)對象
以摩卡(Mocha)對象裝飾它
以奶泡(Whip)對象裝飾它
調用cost()方法,並依賴委託(delegate)將調料的價錢加上去。
具體的實現咱們用一張圖來展現
首先咱們構建DarkRoast對象
假如顧客須要摩卡(Mocha),再創建一個Mocha對象,並用DarkRoast對象包起來。
若是顧客也想要奶泡(Whip),就創建一個Whip裝飾者,並將它用Mocha對象包起來。
最後運算客戶的帳單的時候,經過最外層的裝飾者Whip的cost()就能夠辦獲得。Whip的cost()會委託他的裝飾對象(Mocha)計算出價格,再加上奶泡(Whip)的價格。
經過對星巴茲咖啡的設計方案分析,咱們能夠發現,全部的裝飾類都具有如下幾個特色:
裝飾者和被裝飾對象有相同的超類型。
你能夠用一個或多個裝飾者包裝一個對象。
既然裝飾者和被裝飾對象有相同的超類型,因此在任何須要原始對象(被包裝的)的場合,能夠用裝飾過的對象代替它。
裝飾者能夠在所委託被裝飾者的行爲以前與/或以後,加上本身的行爲,以達到特定的目的。
對象能夠在任什麼時候候被裝飾,因此能夠在運行時動態地、不限量地用你喜歡的裝飾者來裝飾對象
什麼是裝飾模式呢?咱們首先來看看裝飾模式的定義:
裝飾者模式動態地將責任附加到對象上。 若要擴展功能,裝飾者提供了比繼承更有彈性 的替代方案。
定義雖然已經定義了裝飾者模式的「角色」,可是未說明怎麼在咱們的實現中如何使用它們。咱們繼續在星巴茲咖啡中來熟悉相關的操做。
其中裝飾者層級能夠無限發展下去,不是如圖中通常兩層關係。而且組件也並不是只有一個,能夠存在多個。
如今咱們就在星巴茲咖啡裏運用裝飾者模式:
到這裏,咱們隊裝飾者模式已經有了一個基本的認識。那麼咱們已經解決了上面提到的四個問題:
那麼根據第四個問題,假如咱們須要雙倍摩卡豆漿奶泡拿鐵咖啡時,該如何去運算帳單呢?首先,咱們先把前面的深度烘焙摩卡咖啡的設計圖放在這裏。
而後咱們只須要將Mocha的裝飾者加一,便可
前面已經把設計思想都設計出來了,接下來是將其具體實現了。首先從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的程序員來講就增長了學習難度和學習成本。可是,懂得裝飾者模式之後會很是容易理解和設計相關的類。
在理解裝飾者模式和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的內容能夠自行編輯,放在項目根目錄下個人內容原文爲:
運行結果爲:
至此,咱們已經掌握了裝飾者模式的相關知識點。總結一下:
第五設計原則 類應該對拓展開放,對修改關閉。
裝飾者模式動態地將責任附加到對象上。 若要擴展功能,裝飾者提供了比繼承更有彈性 的替代方案。
相應的資料和代碼託管地址github.com/jixiang5200…