天天看點

《Head First 設計模式》模式3——裝飾者模式為Starbuzz開發訂單系統以裝飾者構造飲料訂單重構後的訂單系統實作裝飾者模式裝飾者模式應用者——Java I/O

概念什麼的都是正确的廢話!

是以不說廢話,直接上栗子:

為Starbuzz開發訂單系統

Starbuzz是一家咖啡連鎖店,他們準備更新訂單系統,以适應新品類的咖啡和調料的不斷加入,達到供應需求。

Starbuzz改進了他們原先的系統,由飲料基類

Beverage

開始,在

Beverage

裡有各種調料,

cost()

方法計算飲料所用調料的總價錢,然後由子類(比如:

HouseBlend

(綜合)、

DarkRoast

(超優深焙咖啡豆)、

Decaf

(低咖啡因)、

Espresso

(濃縮咖啡))在覆寫基類的

cost()

,計算調料價錢和咖啡價錢的總和。

飲料基類

Beverage

的Java代碼為:

public class Beverage {
    String description = "Unkown Beverage";

    private boolean milk;
    private boolean soy;
    private double milkCost;
    private double soyCost;

    public Beverage() {
    }

    public String getDescription(){
        return this.description;
    }

    public double cost() {
        double sum = ;
        if (hasMilk())
            sum += milkCost;
        if (hasSoy())
            sum += soyCost;
        return sum;
    }

    public boolean hasMilk() {
        return milk;
    }
    public boolean hasSoy() {
        return soy;
    }

    // milk、soy等調料的getter和setter方法
}
           

咖啡品種以

DarkRoast

為例:

public class DarkRoast extends Beverage {
    public DarkRoast() {
        description = "Most Excellent Dard Roast";
    }

    public double cost() {
        return  + super.cost();
    }
}
           

看起來好像可以應付所需,但是存在一些潛在問題:變化的部分(調料)一改變就涉及修改,不能動态地添加新功能!

有沒有更好地辦法改進呢?

以裝飾者構造飲料訂單

從上面的改造可以看到,當新功能出現時(添加新的調料)時,就要修改

Beverage

的代碼,不能動态地組合對象以避免修改。這是不符合“開閉原則”的!

此處引出第5個設計原則:開放-關閉原則

類應該對擴充開放,對修改關閉

哎,前面的設計太死闆,基類不能加入新功能以适應所有子類,太失敗了!

經過思考,我們決定重新改造:以飲料為主體,在運作時以調料來“裝飾”(decorate)飲料。比方說,如果顧客想要一杯深焙咖啡,加上摩卡和奶泡調料,那麼做法是:

  • 拿一個深焙咖啡(DarkRoast)對象
  • 以摩卡(Mocha)調料對象裝飾它
  • 以奶泡(Whip)調料對象裝飾它
  • 調用cost()方法,并依賴委托(delegate)調料的價錢加上去

PS:此處的“委托”并不是類似C#文法中的delegate,而是指傳遞性的調用上一級的方法。

從圖中我們可以看到“裝飾”的過程:

《Head First 設計模式》模式3——裝飾者模式為Starbuzz開發訂單系統以裝飾者構造飲料訂單重構後的訂單系統實作裝飾者模式裝飾者模式應用者——Java I/O

值得注意的是,調料

Mocha

Whip

也是

Beverage

類型,因為他們要裝飾的對象是

Beverage

的子類,而它們本身亦可以被其他調料裝飾。

由此構造我們可以知道:

  • 裝飾者和被裝飾對象具有相同的超類型
  • 可以用一個或多個對象包裝一個對象
  • 裝飾者可以在所委托被裝飾者的行為前後加上自己的行為,達到特定目的
  • 對象可以在任何時候被裝飾,是以可以在運作動态地用各種裝飾者來裝飾它

看來這個想法可行,我們嘗試設計一下裝飾者和被裝飾者之間的關系類圖:

《Head First 設計模式》模式3——裝飾者模式為Starbuzz開發訂單系統以裝飾者構造飲料訂單重構後的訂單系統實作裝飾者模式裝飾者模式應用者——Java I/O

此處,

Decorator

(裝飾者)裡有一個

Component

(被裝飾者)對象,用來儲存對某個

Component

的引用。這樣其子類(各種具體裝飾者)就可以裝飾任何

Component

對象了~

重構後的訂單系統實作

好了,我們将上面新的設計引進到Starbuzz的訂單系統中,重新設計飲料和調料的類結構:

《Head First 設計模式》模式3——裝飾者模式為Starbuzz開發訂單系統以裝飾者構造飲料訂單重構後的訂單系統實作裝飾者模式裝飾者模式應用者——Java I/O

可能有人會有疑問:在政策模式和觀察者模式裡講到,不是要多用組合少用繼承嗎,為什麼這裡是通過繼承來動态地改變功能呢?

其實,裝飾者和被裝飾者必須是一樣的類型,因為裝飾者在裝飾對象時它本身亦可以被其它裝飾者裝飾。是以,我們利用繼承是想達到“類型比對”,而不是通過繼承獲得“行為”。這點很重要!

如此一來,當裝飾者和被裝飾者組合時,就相當于動态地添加行為。這樣得到的新行為,不是繼承超類而是組合對象得到的。

下來通過代碼來體驗一下訂單系統的類新結構:

首先建立基類

Beverage

// 飲料基類
public abstract class Beverage {
    protected String descrption;

    public String getDescription(){
        return descrption;
    }

    public abstract double cost();
}
           

再建立調料的抽象類

CondimentDecorator

// 調料基類
public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}
           

飲料和調料基礎類都有了,下面開始實作一些飲料:

// 超優深焙咖啡豆
public class DarkRoast extends Beverage {   
    DarkRoast() {
        descrption = "DarkRoast";
    }

    public double cost() {
        return ;
    }
}
           
// 濃縮咖啡
public class Espresso extends Beverage {    
    Espresso() {
        descrption = "Espresso";
    }

    public double cost() {
        return ;
    }
}
           
// 低咖啡因咖啡
public class Decaf extends Beverage {   
    Decaf() {
        descrption = "Decaf";
    }

    public double cost() {
        return ;
    }
}
           

再實作一些調料:

// 摩卡調料
public class Mocha extends CondimentDecorator {
    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Mocha";
    }

    public double cost() {
        return  + beverage.cost();
    }
}
           
// 豆漿調料
public class Soy extends CondimentDecorator {
    Beverage beverage;

    public Soy(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Soy";
    }

    public double cost() {
        return  + beverage.cost();
    }
}
           
// 奶泡調料
public class Whip extends CondimentDecorator {
    Beverage beverage;

    public Whip(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Whip";
    }

    public double cost() {
        return  + beverage.cost();
    }
}
           
// 牛奶調料
public class Milk extends CondimentDecorator {
    Beverage beverage;

    public Milk(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Milk";
    }

    public double cost() {
        return  + beverage.cost();
    }
}
           

最後來測試一下新的訂單系統是否計算正确:

public class StarbuzzCoffee {
    public static void main(String[] args) {
        // 濃縮咖啡(不加調料)
        Beverage beverage = new Espresso();
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

        // 深焙咖啡 + 雙倍摩卡 + 奶泡
        Beverage beverage2 = new DarkRoast();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Mocha(beverage2);
        beverage2 = new Whip(beverage2);
        System.out.println(beverage2.getDescription() + " $" + beverage2.cost());

        // 低咖啡因咖啡 + 豆漿 + 摩卡 + 奶泡
        Beverage beverage3 = new Decaf();
        beverage3 = new Soy(beverage3);
        beverage3 = new Mocha(beverage3);
        beverage3 = new Milk(beverage3);
        System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
    }
}
           

控制台的列印結果是:

Espresso $0.
DarkRoast, Mocha, Mocha, Whip $2.
Decaf, Soy, Mocha, Milk $1.
           

不僅計算正确,而且這全新設計的訂單系統設計也符合Starbuzz的需求!

當然,這裡建立裝飾者還不夠簡潔,待到後面接觸“工廠模式”時會介紹更好的方式建立裝飾者對象~

裝飾者模式

吃完栗子可以講正确的廢話了!

裝飾者模式的定義:

動态地将責任附加到對象上,若要擴充功能,裝飾者提供了比繼承者更有彈性的替代方案。

裝飾者模式應用者——Java I/O

我們知道,java.io包裡有很多類,其實很多都是裝飾者。

比如:用

BuffereadInputStream

裝飾

FileInputStream

,用

LineNumberInputStream

裝飾

BuffereadInputStream

。。。

為了更熟悉裝飾者模式,我們自己也來寫一個裝飾者——把輸入流内的所有大寫字元轉換成小寫字元。

import java.io.*;

// 自己的java I/O 裝飾者:将大寫字元轉小寫
public class LowerCaseInputStream extends FilterInputStream {

    public LowerCaseInputStream(InputStream in) {
        super(in);
    }

    public int read() throws IOException {
        int c = super.read();
        return (c == - ? c : Character.toLowerCase((char)c));
    }

    public int read(byte[] b, int offset, int len) throws IOException {
        int result = super.read(b, offset, len);
        for (int i = offset; i < offset+result; i++) {
            b[i] = (byte)Character.toLowerCase((char)b[i]);
        }
        return result;
    }
}
           

測試一把:

public class InputTest {
    public static void main(String[] args) throws IOException {
        int c;

        try {
            InputStream in = 
                new LowerCaseInputStream(
                    new BufferedInputStream(
                        new FileInputStream("test.txt")));

            while((c = in.read()) >= ) {
                System.out.print((char)c);
            }

            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
           

繼續閱讀