裝飾對象
java
咱們即將討論典型的繼承濫用問題。並學到如何使用對象組合的方式,作到在運行時裝飾類。api
用一個簡單的需求來描述問題,星巴茲(Starbuzz)須要準備訂單系統,這是他們的第一個嘗試,類設計是這樣的:框架
Beverage(飲料)是一個抽象類,店內所提供的飲料都必須繼承自此類,這個類有一個description(描述)的實例變量,能夠由每一個子類設置,用來描述飲料,例如:超優深培咖啡豆(Dark Roast)等。cost()方法是抽象的,子類必須定義本身的實現。每一個子類都實現cost()來返回飲料的價錢。購買咖啡時,也能夠要求在其中加入各類調料,例如:蒸奶、豆漿、摩卡或覆蓋奶泡。咖啡店會根據加入的調料收取不一樣的費用。ide
代碼與客戶端調用以下:函數
package cn.net.bysoft.decorator; // 飲料的基類。 public abstract class Beverage { // 計算價格的方法。 public abstract double cost(); // 飲料描述的getter and setter。 public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } // 飲料的描述。 private String description = ""; }
package cn.net.bysoft.decorator; // 深焙咖啡對象。 public class DarkRoast extends Beverage { @Override public double cost() { // 牛奶 + 糖 + 深焙咖啡豆 = 1$ return 1.00D; } }
package cn.net.bysoft.decorator; // 濃縮咖啡對象。 public class Espresso extends Beverage { @Override public double cost() { // 濃縮咖啡價格是1.15$。 return 1.15D; } }
package cn.net.bysoft.decorator; public class Client { public static void main(String[] args) { // 來一杯濃縮咖啡。 Beverage espresson = new Espresso(); System.out.println("濃縮咖啡的價格是:" + espresson.cost() + "$"); // 來一杯深焙咖啡。 Beverage darkRoast = new DarkRoast(); System.out.println("深焙咖啡的價格是:" + darkRoast.cost() + "$"); /** * output: * 濃縮咖啡的價格是:1.15 * 深焙咖啡的價格是:1.0 * */ } }
很明顯,咖啡店爲本身製造了一個維護的噩夢,看類圖,假如咖啡店有82種咖啡,若是牛奶的價錢上揚,須要改動全部與牛奶有關的咖啡的價格,若是咖啡豆價格上調的話……若是新增一種全部飲料都試用的焦糖風味,全部類都要改變……測試
這時候,有些人已經想到了解決辦法,利用實例變量與繼承,就能夠追蹤這些調料。this
好吧,就來試試看。先從Beverage基類下手,加上實例變量表明是否加上調料,如今,Beverage類中的cost()再也不是一個抽象方法,咱們提供了cost()的實現,讓他計算要加入各類飲料的調料價錢。子類仍覆蓋cost(),可是會調用超類的cost(),計算出基本飲料加上調料的價錢。看一下代碼如何實現:spa
package cn.net.bysoft.decorator; // 飲料的基類。 public abstract class Beverage { // 基類計算全部調料的價錢。 public double cost() { double price = 0.0D; if (hasMilk()) { price += 0.2D; } if (hasSoy()) { price += 0.2D; } if (hasMocha()) { price += 0.3D; } if (hasWhip()) { price += 0.3D; } return price; } // 飲料描述的getter and setter。 public String getDescription() { return description; } public boolean hasMilk() { return milk; } public void setMilk(boolean milk) { this.milk = milk; } // getter and setter... // 飲料的描述。 private String description = ""; private boolean milk = false; private boolean soy = false; private boolean mocha = false; private boolean whip = false; }
package cn.net.bysoft.decorator; // 深焙咖啡對象。 public class DarkRoast extends Beverage { @Override public double cost() { // 深焙咖啡使用牛奶。 double price = 0.0D; super.setMilk(true); // 計算了牛奶的價錢,在加上深焙咖啡的原料。 // 0.2 + 1.05 = 1.25; price = super.cost() + 1.05; return price; } }
package cn.net.bysoft.decorator; public class Client { public static void main(String[] args) { // 來一杯深焙咖啡。 Beverage darkRoast = new DarkRoast(); System.out.println("深焙咖啡的價格是:" + darkRoast.cost() + "$"); /** * output: * 深焙咖啡的價格是:1.25$ * */ } }
這種作法,很是容易的就解決的前一種設計的問題,咖啡店很滿意,開始使用修改後的訂單系統。.net
過了一陣子,發現這種設計並不能知足平常應用,好比:設計
調料價格的改變仍是要修改現有代碼;
一旦出現新的調料,就須要加上新的hasXXX方法,並改變超類中的cost()方法;
之後可能會開發出新飲料,對於這些飲料(例如冰茶等),某些調料可能並不合適,可是在這個設計方式彙總,Tea(茶)子類仍然繼承哪些不合適的方法,好比hasWhip(加奶泡);
萬一顧客想要雙倍的摩卡或者雙倍的糖怎麼辦;
此刻,就面臨了最重要的設計原則之一:
設計原則
類應該對擴展開放,對修改關閉。
咱們的目標是容許類容易擴展,在不修改現有代碼的狀況下,就能夠配置新的行爲。如能實現這樣的目標,有什麼好處呢?這樣的設計具備彈性能夠應對改變,能夠接受新的功能來應對需求變動。
認識裝飾者模式
好了,咱們已經瞭解到利用繼承沒法徹底解決問題,在咖啡館遇到的問題有:類數量爆炸、設計死板,以及基類加入的新功能並不適用於全部的子類。
因此,在這裏要採用不同的作法:咱們要以飲料爲主題,而後在運行時以調料來「裝飾」(decorate)飲料。比方說,若是顧客想要摩卡和奶泡的深培咖啡,那麼,要作的是:
拿一個深培咖啡(DarkRoast)對象;
以摩卡(Mocha)對象裝飾它;
以奶泡(WHip)對象裝飾它;
調用cost()方法,並依賴委託(delegate)將調料的價錢加上去;
這樣就完工了一個深培咖啡對象,可是如何「裝飾」一個對象,而「依賴委託」又要如何與此搭配使用呢?
裝飾者(調料)和被裝飾者(飲料)都有相同的超類;
能夠用一個或多個裝飾者(調料)包裝一個被裝飾者(飲料);
既然裝飾者和被裝飾者都有相同的超類,那麼在任何須要原始對象,也就是被裝飾者(飲料)的場合,均可以使用裝飾過的對象(飲料)代替它;
裝飾者能夠在所委託的被裝飾者的行爲以前或以後,加上本身的行爲,以達到特定的目的;
對象能夠在任什麼時候候被裝飾,因此能夠在運行時動態地、不限量地用你喜歡的裝飾者來裝飾對象;
如今,就來卡看裝飾者模式的定義,並寫一些代碼,瞭解它究竟是怎麼工做的吧!先來看看裝飾者模式的類圖:
Component(組件)類:每一個組件均可以單獨使用,或者被裝飾者包裝起來使用。
ConcreteComponent(被裝飾組件)類:咱們將要動態地加上新行爲的對象,它拓展自Component類。
Decorator(裝飾)類:每個裝飾者都「有一個」(包裝一個)組件,也就是說,裝飾者有一個實例變量以保存某個Component的引用。
ConcreteDecoratorXX類:有一個實例變量能夠記錄所裝飾的事物,還能夠加上新的方法。
如今,讓咖啡店的訂單系統也符合此框架:
左側4個類HouseBlend、DarkRoast、Decaf、Espresso是具體的組件類,而右下角的4個類Milk、Mocha、Soy、Whip是具體的裝飾者類,這麼一看並無感受到裝飾者有多厲害,讓咱們用代碼來實現之後在看看效果,如今的需求爲:「來一杯雙倍摩卡豆漿奶泡拿鐵咖啡」,使用訂單系統獲得正確的價錢:
package cn.net.bysoft.decorator; // 飲料的基類。 public abstract class Beverage { // 飲料的說明。 String description = "Unknow Beverage"; public String getDescription() { return description; } public abstract double cost(); }
第一個與咱們見面的類是飲料的基類,有兩個方法,分別是getDescription()和cost(),用來返回飲料說明和價格。
package cn.net.bysoft.decorator; // 調料對象。 public abstract class CondimentDecorator extends Beverage { // 飲料描述。 public abstract String getDescription(); }
第二個出現的類是調料的抽象類,首先,必須讓該類能取代Beverage,因此繼承自Beverage類。
它的方法getDescription()必須讓全部的調料類都實現。
如今,基類已經有了,開始實現一些飲料吧!先從濃縮咖啡開始。
package cn.net.bysoft.decorator; // 濃縮咖啡對象。 public class Espresso extends Beverage { public Espresso() { super.description = "Espresso Coffee"; } @Override public double cost() { return 1.99D; } }
首先,讓Espresso拓展自Beverage類,由於濃縮咖啡也是一種飲料。而後設置濃縮咖啡的說明屬性description,最後使用cost()方法返回價格。
再實現一個黑咖啡(HouseBlend)和深焙咖啡(DarkRoast),代碼同上,其他的飲料都同樣。
package cn.net.bysoft.decorator; // 黑咖啡對象。 public class HouseBlend extends Beverage { public HouseBlend() { super.description = "House Blend Coffee"; } @Override public double cost() { return .89; } }
package cn.net.bysoft.decorator; // 深焙咖啡對象。 public class DarkRoast extends Beverage { public DarkRoast() { super.description = "DarkRoast Coffee"; } @Override public double cost() { return .89; } }
寫好具體的飲料對象後,就能夠着手調料類的編寫了:
package cn.net.bysoft.decorator; public class Mocha extends CondimentDecorator { // 須要裝飾的類。 Beverage beverage; public Mocha(Beverage beverage) { this.beverage = beverage; } @Override public String getDescription() { return beverage.getDescription() + ", Mocha"; } @Override public double cost() { return .20 + beverage.cost(); } }
摩卡(Mocha)是一個裝飾者,拓展自CondimentDecorator類,也就是說,摩卡也是一個Beverage類。
摩卡對象中有一個Beverage對象用於存放要封裝的具體飲料類,目前有Espresso和HouseBlend兩個飲料。
在構造函數中把具體飲料看成參數傳遞到Mocha中。
在描述時,不僅是調用傳遞進來的飲料的描述,還能夠加入本身想要加入的描述(例如「DarkRoast, Mocha」)。
最後cost()方法,首先調用傳遞進來的飲料的cost()方法,得到價格,在加上Mocha本身的錢,獲得最終結果。
在最後測試以前,實現奶泡條件,完成需求:
package cn.net.bysoft.decorator; // 奶泡調料 public class Whip extends CondimentDecorator { // 須要裝飾的類。 Beverage beverage; public Whip(Beverage beverage) { this.beverage = beverage; } @Override public String getDescription() { return beverage.getDescription() + ", Whip"; } @Override public double cost() { return .20 + beverage.cost(); } }
下面就是使用訂單的一些測試代碼:
package cn.net.bysoft.decorator; public class Client { public static void main(String[] args) { // 訂一杯濃縮咖啡,不須要調料,打印價格。 Beverage beverage = new Espresso(); System.out.println(beverage.getDescription() + " $" + beverage.cost()); System.out.println(); // 訂一杯深焙咖啡,加入雙倍的Mocha,在加入奶泡。 // 由於調料與飲料都是擴展自Beverage,因此可使用下面的等式。 Beverage beverage2 = new DarkRoast(); beverage2 = new Mocha(beverage2); beverage2 = new Mocha(beverage2); beverage2 = new Whip(beverage2); System.out .println(beverage2.getDescription() + " $" + beverage2.cost()); System.out.println(); /** * output: * Espresso Coffee $1.99 * * DarkRoast Coffee, Mocha, Mocha, Whip $1.49 * */ } }
再來拓展一下需求,如今咖啡店決定開始在菜單上爲全部飲料加入容量,提供小杯(Tall)、中杯(Grande)、大杯(Venti)飲料,每次加入摩卡和奶泡不是按照固定的0.20美金收費了,而是按照,小中大杯的咖啡加摩卡和奶泡時,判斷size,而後分別加收0.一、0.1五、0.2美金。
如何改變裝飾者類對應這樣的需求呢?
首先,在飲料基類中加入SIZE屬性:
public enum BeverageSize { TALL, GRANDE, VENTI }
public abstract class Beverage { BeverageSize size = BeverageSize.TALL; public void setSize(BeverageSize size) { this.size = size; } public String getDescription() { return description; } ... }
而後,修改摩卡和奶泡的cost方法,修改以前將摩卡和奶泡類中的Beverage提取到基類CondimentDecorator中:
// 調料對象。 public abstract class CondimentDecorator extends Beverage { Beverage beverage; public BeverageSize getSize() { return beverage.getSize(); } ... }
package cn.net.bysoft.decorator; public class Mocha extends CondimentDecorator { public Mocha(Beverage beverage) { super.beverage = beverage; } @Override public double cost() { double cost = beverage.cost(); if (getSize() == BeverageSize.TALL) { cost += .10; } else if (getSize() == BeverageSize.GRANDE) { cost += .15; } else if (getSize() == BeverageSize.VENTI) { cost += .20; } return cost; } ... }
奶泡的類,與摩卡類相同,最後進行測試:
public static void main(String[] args) { // 訂一杯深焙咖啡,加入雙倍的Mocha,在加入奶泡。 Beverage beverage2 = new DarkRoast(); // 大杯飲料,每種調料0.15美金。 beverage2.setSize(BeverageSize.GRANDE); beverage2 = new Mocha(beverage2); beverage2 = new Mocha(beverage2); beverage2 = new Whip(beverage2); // 0.89+0.15+0.15+0.15 = 1.34 System.out.println(beverage2.getDescription() + " $" + String.format("%.2f", beverage2.cost())); System.out.println(); /** * output: DarkRoast Coffee, Mocha, Mocha, Whip $1.34 * */ }
以上就是裝飾者模式的所有內容。
另外,java.io包內的類大量的使用了裝飾者模式,好比:
FileInputStrame被BufferedInputStream裝飾。
而BufferedInputStream又被LineNumberInputStream裝飾。
具體的細節能夠查看java的api和源碼。