HeadFirst設計模式(三) - 裝飾者模式

裝飾對象
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)飲料。比方說,若是顧客想要摩卡和奶泡的深培咖啡,那麼,要作的是:

  1. 拿一個深培咖啡(DarkRoast)對象;

  2. 以摩卡(Mocha)對象裝飾它;

  3. 以奶泡(WHip)對象裝飾它;

  4. 調用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和源碼。

相關文章
相關標籤/搜索