headfirst設計模式(3)—裝飾者模式
序
好久沒寫設計模式了,自從寫了兩篇之後,就放棄治療了,主要還是工作太忙了啊(借口,都是借口),過完年以後一直填坑,填了好幾個月,總算是穩定下來了,可以打打醬油了。
為什麽又重新開始寫設計模式呢?學習使我快樂啊(我裝逼起來我自己都害怕),其實主要是最近填坑的時候看源代碼有點暈,很多代碼不知道他們為什麽要那麽寫啊,好氣啊
當時第二篇寫完,其實就在準備第三篇了,但是,一直也沒有寫,看了好幾遍,但是一直掌握不到精髓(其實現在也掌握不到),感覺挺模糊的,也就一直拖啊拖,拖延癥晚期患者已經不用搶救了。。。
先來舉個栗子
故事背景:星巴茲咖啡,由於快速擴展,他們現在的訂單系統已經跟不上他們的飲料供應需求了,先看一下當前的設計
/** * 飲料超類 * @author Skysea * */ public abstract class Beverage { protected String description;//描述 public abstract double cost();//消費金額 public Beverage(String description){ this.description = description; } public String getDescription() { return description; }public void setDescription(String description) { this.description = description; } }
下面是各種咖啡,這裏就只展示一種:
/** * 首選咖啡 * @author Skysea * */ public class HouseBlend extends Beverage{ public HouseBlend(String description) { super(description); } @Override publicdouble cost() { return 3.5; } }
很簡單的代碼,咖啡繼承飲料超類。
啥都不說了,先提需求:
1,購買咖啡時,要求可以在其中加入各種調料,例如:蒸奶,豆漿,摩卡...等等(產品每次給我們說的就是,具體的他們還沒定好,能不能做成活的?就是隨時可以擴展的那種。知道我為啥要來學設計模式了嗎?會被玩死的,真的)
2,各種調料也會參與計價,而且每種調料的價格不一定相同
需求清楚了吧?不清楚?沒關系,反正下周上線,不存在的,嘿嘿嘿
先來給出第一版實現(直接擴展超類):
/** * 飲料超類 * @author Skysea * */ public abstract class Beverage { protected String description;//描述 public abstract double cost();//消費金額 private boolean milk;//牛奶 private boolean soy;//豆漿 private boolean mocha;//摩卡 private boolean whip;//奶泡 //省略get set方法... }
不就是下周上線嗎?不就是要擴展嗎?要多少,就在超類裏面加就好了啊,然後再對每一種飲料進行處理,像這樣:
/** * 首選咖啡 * @author Skysea * */ public class HouseBlend extends Beverage{ public HouseBlend(String description) { super(description); } @Override public double cost() { double cost = 3.5; if(hasMilk()){ cost += 0.5; } if(hasMocha()){ cost += 0.6; } if(hasSoy()){ cost += 0.3; } if(hasWhip()){ cost += 0.4; } return cost; } }
感覺每一種都要寫這麽多if好麻煩啊,不存在的,這種東西,抽出來嘛:
/** * 飲料超類 * @author Skysea * */ public abstract class Beverage { /** * 調料消費 * @return */ protected double flavourCost(){ double cost = 0.0; if(hasMilk()){ cost += 0.5; } if(hasMocha()){ cost += 0.6; } if(hasSoy()){ cost += 0.3; } if(hasWhip()){ cost += 0.4; } return cost; } //... }
子類:
/** * 首選咖啡 * @author Skysea * */ public class HouseBlend extends Beverage{ public HouseBlend(String description) { super(description); } @Override public double cost() { return 3.5 + flavourCost(); } }
感覺也挺好的啊,每次要添加的時候,先去超類添加一個屬性,然後在超類的 flavourCost()方法中,添加一段代碼,以前的子類完全不用動,邏輯也是妥妥的,一切都是很OK的
但是,這樣寫至少有三處是不符合邏輯的:
1,所有的子類的cost方法都必須要加上一句 xxx + flavourCost(),不覺得寫的次數太多了嗎?一般一個方法寫N次,那大部分最後會變成坑
2,超類的所有屬性並不是每一個子類都能用上的
3,違背了開閉原則,每一次擴展雖然不用修改子類,但是卻會去修改父類的屬性,以及父類的flavourCost()方法
在現在的這個需求下,這些不合理都是體現不出來有什麽問題的,但是,代碼中不符合邏輯的東西早晚有一天會對整個模塊的設計造成巨大的影響,兩種情況除外:
1,你不幹了(錢沒給夠或者我不開心畢竟程序員都是非常任性的),對模塊的影響跟你沒有半毛錢的關系
2,這個模塊不擴展,不維護,或者擴展、維護還沒有達到臨界點(時間根據前期邏輯混亂程度成反比)
別問我怎麽知道的,因為我TM還沒走,所以就來學習來了,我擦,跑題了。。。
下面來聊聊剛學的正確姿勢:
裝飾者模式
先說超類 Beverage(不變,保持最最原始的樣子):
/** * 飲料超類 * @author Skysea * */ public abstract class Beverage { protected String description;//描述 public abstract double cost();//消費金額 public Beverage(String description){ this.description = description; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
為什麽不變?
要回答這個問題,首先要考慮,整個模塊中,到底是什麽東西一直變來變去?沒錯,就是調料啊。
一般變化的怎麽處理?
抽離出來,把它和穩定的東西抽離出來,即保證了它的擴展性,又提高了代碼的穩定性,遵循了開閉原則
所以為什麽不變?1,它很穩定。2,它很穩定。沒有3,我要逼死強迫癥
接下來,貼一下它的子類代碼,調整的地方:
public HouseBlend(String description) { super(description); }
把 description放在構造函數內寫死,為啥?你創建一個咖啡叫啥名字,難道還要客戶端幫你想好?
讓他去做也是可以的,但是有兩個問題你要想清楚:
1,他知道你住哪裏嗎?
2,你走夜路嗎?
臥槽,走遠了走遠了...
繼續:
/** * 首選咖啡 * @author Skysea * */ public class HouseBlend extends Beverage{ public HouseBlend() { super("首選咖啡"); } @Override public double cost() { return 3.5; } }
為了保證最後的測試類可以正常的跑,我把其他的也寫一些出來...
/** * 濃咖啡 * @author Skysea * */ public class Espresso extends Beverage { public Espresso() { super("濃咖啡"); } @Override public double cost() { return 2.5; } }
/** * 焦炒咖啡 * @author Skysea * */ public class DarkRoast extends Beverage{ public DarkRoast() { super("焦炒咖啡"); } @Override public double cost() { return 3.2; } }
然後來到最重要的裝飾超類CondimentDecorator (第一版):
/** * 調料裝飾類 * @author Skysea * */ public abstract class CondimentDecorator extends Beverage { protected Beverage beverage; public CondimentDecorator(String description) { super(description); } //重寫cost方法 @Override public String getDescription(){ return this.beverage.getDescription() + ", " +this.description; } //直接實現cost方法 public double cost(){ return this.beverage.cost() + condimentCost(); } protected abstract double condimentCost();//重新抽象一個調料的價格方法 }
裝飾子類(Mocha ):
/** * 調料:摩卡 * @author Skysea * */ public class Mocha extends CondimentDecorator { public Mocha() { super("摩卡"); } @Override protected double condimentCost() { return 0.6; } }
是不是感覺很簡單?所有的邏輯都在裝飾超類裏面處理好了,感覺妥妥的,沒什麽不對,而且其他的子類實現起來也是非常的輕松
上面的裝飾類是我自己的第一版實現,寫完之後,我發現,這個東西,和我們最初想要的裝飾類有很大的區別:
不靈活
現在我們來看看裝飾者模式的定義:
裝飾模式指的是在不必改變原類文件和使用繼承的情況下,動態地擴展一個對象的功能。它是通過創建一個包裝對象,也就是裝飾來包裹真實的對象。(百度百科)
想要應對越復雜的變化,那麽就要給子類賦予越大的權限,讓它接觸越多的東西,只有這樣才能做靈活,當然,靈活也是相對的,這個也是必須在實際項目中取舍
所以,上面的裝飾類實現,根本無法滿足當前需求所需要的靈活性,比如:使用摩卡的時候,所有的飲料滿20減5塊怎麽玩?是不是感覺玩不動了?
問題在哪裏呢?
//直接實現cost方法 public double cost(){ return this.beverage.cost() + condimentCost(); }
就是在這裏,這個地方把cost方法定的太死板了,也不能說這樣不對,只是,這樣所有繼承CondimentDecorator的其實只能算裝飾者模式的特例,要實現這樣的東西,就必須在CondimentDecorator與Beverage中間再抽象一層。
就像這樣CondimentDecorator extends Decorator,Decorator extends Beverage,其他的調料繼承 Decorator (咳,別聽我亂BB)
講講正確的裝飾類及實現吧:
/** * 調料裝飾類 * @author Skysea * */ public abstract class CondimentDecorator extends Beverage { protected Beverage beverage; public CondimentDecorator(Beverage beverage, String description){ super(description); this.beverage = beverage; } //根據情況來覺得是不是要抽象出來讓子類實現,原因和剛才抽象condimentCost()一樣,分情況,沒有什麽是對不對的 @Override public String getDescription(){ return this.beverage.getDescription() + ", " +this.description; } }
調料實現類:
/** * 調料:牛奶 * @author Skysea * */ public class Milk extends CondimentDecorator{ public Milk(Beverage beverage) { super(beverage, "牛奶"); } @Override public double cost() { return 0.5 + beverage.cost(); } }
/** * 調料:摩卡 * @author Skysea * */ public class Mocha extends CondimentDecorator { public Mocha(Beverage beverage) { super(beverage, "摩卡"); } @Override public double cost() { return 0.6 + beverage.cost(); } }
/** * 調料:豆漿 * @author Skysea * */ public class Soy extends CondimentDecorator{ public Soy(Beverage beverage) { super(beverage, "豆漿"); } @Override public double cost() { return 0.3 + beverage.cost(); } }
....
測試類:
/** * 裝飾模式測試類 * @author Skysea * */ public class Test { public static void main(String[] args) { //初始化濃咖啡 Beverage beverage = new Espresso();//$2.5 System.out.println(beverage.getDescription() + " $"+ beverage.cost()); //初始化焦炒咖啡 Beverage beverage2 = new DarkRoast();//$3.2 //用調料來裝飾它 beverage2 = new Mocha(beverage2);//$0.6 beverage2 = new Mocha(beverage2);//$0.6 beverage2 = new Soy(beverage2);//$0.3 System.out.println(beverage2.getDescription() + " $"+ beverage2.cost()); //初始化焦炒咖啡 Beverage beverage3 = new HouseBlend();//3.5 beverage3 = new Mocha(beverage3);//$0.6 beverage3 = new Milk(beverage3);//$0.5 beverage3 = new Soy(beverage3);//$0.3 System.out.println(beverage3.getDescription() + " $"+ beverage3.cost()); } }
運行結果:
為了證明我沒有隨便拿個假截圖來騙你們,我特意把價格都標到測試類後面的
為啥後面那麽長一串?這肯定不是我的鍋啊,double的鍋,推薦用BigDecimal來玩這個,當然也可以用long型,或者int都是可以的,實際項目也是這樣
用什麽主要看:
1,精度要求高不高
2,速度要求快不快
3,看產品會不會讓你保留小數點2位和保留小數點3位換著來玩的情況(都是淚,不說了..)
headfirst設計模式(3)—裝飾者模式