學好設計模式防被祭天:裝飾者模式

weixin_34146805發表於2017-08-27
2405011-36e5013dab91e146.jpeg
裝飾者模式

為了防止被“殺”了祭天,學點設計模式,並總結下還是有必要的。

一:理解

  1. 顧名思義,裝飾者模式可以在不修改基礎類的前提下,增加/修改物件的屬性。
  2. 提到裝飾者模式,立即想到《Head First 設計模式》中,奶茶的例子。
  3. 裝飾者模式的新建過程大致為A a = new C(new B(new A()))。
  4. 無論裝飾多少層,都可以賦值給a物件,表示裝飾者和被裝飾者屬於同一個繼承體系。


二:例子

你是個低調的富二代,不抽菸,不喝酒,沒事就喜歡在街頭走一走,喔哦哦,你會把手揣進褲兜。

2405011-75289d0a89a787b4.jpg
火爆的奶茶店

有一天,你發現街邊有一家叫一丟丟的奶茶店超級火爆。

於是,你決定投資一個億,開一家飲料店。

你叫來程式設計師小菜幫忙設計飲料店系統。

小菜自稱一把梭,上來就是幹,立馬新建了一個飲料基類,如下:

public abstract class Drink {
    
    public abstract String getDesc();

    public abstract double getCost();
}

飲料基類擁有兩個抽象方法,一個返回飲料名稱,一個返回飲料價格。

2405011-5744c76993395507.jpg
一開口就知道是老江湖了

看到小菜這麼積極主動,並且用上了抽象類。你也裝作很懂的樣子,說了句:一上手就知道是老江湖了。

於是你很放心地讓小菜設計接下來的內容。

你的需求是:

  1. 飲料店會售賣4種飲料,包括奶茶(MilkTea),咖啡(Coffee),果汁(Juice),蘇打水(Soda)。
  2. 可以在飲料上加奶泡(Whip),糖霜(Sugar),珍珠(Pearl)。

小菜覺得飲料的種類和加料不是很多,三下五除二就把整個程式給搞定了。

// 奶茶類
public class MilkTea extends Drink {
    @Override
    public String getDesc() {
        return "奶茶";
    }

    @Override
    public double getCost() {
        return 10;
    }
}

// 奶茶加奶泡類
public class MilkTeaWithWhip extends Drink {
    @Override
    public String getDesc() {
        return "奶茶加奶泡";
    }

    @Override
    public double getCost() {
        return 10 + 2;
    }
}

// 奶茶加奶泡加糖霜類
public class MilkTeaWithWhipAndSugar extends Drink {
    @Override
    public String getDesc() {
        return "奶茶加奶泡加糖霜";
    }

    @Override
    public double getCost() {
        return 10 + 2 + 1;
    }
}

// 奶茶加奶泡加糖霜加珍珠類
public class MilkTeaWithWhipAndSugarAndPearl extends Drink {
    @Override
    public String getDesc() {
        return "奶茶加奶泡加糖霜加珍珠";
    }

    @Override
    public double getCost() {
        return 10 + 2 + 1 + 1;
    }
}

// 奶茶加糖霜類
// 奶茶加糖霜加珍珠類
// ……
// 咖啡加XXX類
// ……

搞定了奶茶MilkTea類,奶茶可以不加料,加一種料,兩種料,三種料,一共是1 + 3 + 3 + 1 = 8個類。

小菜接著搞定咖啡,果汁,蘇打水類,一種是4 * 8 = 32個類,加上基類Drink,一共是33個類。

2405011-ae80b2d947ef7a55.gif
下班

於是,小菜在一片需求不飽和的議論聲中,在六點準時下班了,並期待第二天得到富二代的表揚。

飲料店順利開張了,和意料中的一樣,生意超級火爆。

多了一個奶茶店店主title的你,很是高興,一有空就去隔壁老王的奶茶店調研。

你發現王叔叔奶茶店可以讓顧客選擇不同的甜度,從不加糖加十分甜,一共11個等級。

你覺得很有創意,於是你迫不及待地叫小菜加上這個功能。

小菜沒多想,立馬答應了下來,並表示在做完手頭的需求之後,立馬就做。

夜深人靜,小菜拿出鍵盤,準備一段敲。

突然發現,原來有32個類,每個類都需要擴充套件成從0級甜度到10級甜度。

一算下來,類會被擴充套件到32 * 11=352。

不過富二代第二天就要看到結果,小菜還是決定先做完功能性需求再說。

於是啪啪啪一頓敲,終於寫完了這352個類。

然而第二天,小菜接到通知,糖霜的價格上漲,加糖霜從一塊變成了兩塊。

不會全文搜尋的小菜修改了所有加了糖霜的飲料類之後,累翻在電腦桌前。

儘管如此,小菜還是忘改了一個奶茶加糖霜加A加B加C加D加E加F……類。

在收益計算的時,你發現少了一些錢,瞬間感覺自己損失了兩個億。

於是準備殺了這位程式設計師祭天。

小菜急忙解釋道自己立馬開始重構,一個類就能搞定。

@Data
public class DrinkRe {
    private boolean hasWhip;
    private boolean hasSugar;
    private boolean hasPearl;
    private int sugarIndex;
    private int cost;
    private String name;

    public DrinkRe(boolean hasWhip, boolean hasSugar, boolean hasPearl, int sugarIndex, int cost, String name) {
        this.hasWhip = hasWhip;
        this.hasSugar = hasSugar;
        this.hasPearl = hasPearl;
        this.sugarIndex = sugarIndex;
        this.cost = cost;
        this.name = name;
    }

    public String getDesc() {
        StringBuilder drinkDesc = new StringBuilder();
        drinkDesc.append(name);
        if (hasWhip) {
            drinkDesc.append("加奶泡");
        }
        if (hasSugar) {
            drinkDesc.append("加糖霜");
        }
        if (hasPearl) {
            drinkDesc.append("加珍珠");
        }
        if (sugarIndex >= 0) {
            drinkDesc.append(sugarIndex + "分甜");
        }
        return drinkDesc.toString();
    }

    public double getSumCost() {
        int sumCost = cost;
        if (hasWhip) {
            sumCost += 2;
        }
        if (hasSugar) {
            sumCost += 2;
        }
        if (hasPearl) {
            sumCost += 1;
        }
        return sumCost;
    }

    public static void main(String[] args) {
        DrinkRe coffee = new DrinkRe(true, true, true, 7, 10, "咖啡");
        System.out.println(coffee.getDesc());
        System.out.println(coffee.getSumCost());
    }
}

這個飲料類在構造時傳入基礎飲料的名稱和價格,並在輸出描述和價格之前都先判斷是否有加料。

又過了幾天,飲料店愈發火爆,你開始提出新的需求:

  1. 飲料增加20種。
  2. 加料增加30種。
  3. 加糖霜描述之後加上(木糖醇)字樣,以示很健康。

小菜想了下:

  1. 一個類的情況在構造飲料時,每次都要手動輸入飲料的名字和價格。
  2. hasXXX,在DrinkRe中增加30個屬性,一個好長的建構函式。
  3. 需要寫n多個if判斷語句。
  4. 需求三倒是容易搞定,但又擔心之後又要改回來。

於是,一臉懵X的小菜請教了資深老司機,老司機先開了一會車,不緊不慢地告訴他,裝飾者模式可以解決這個問題。

裝飾者模式包含:

  1. 基類Drink,申明瞭飲料類有getDesc和getCost兩個方法。
  2. 被裝飾者,如基礎款奶茶MilkTea,實現了兩個抽象方法。
  3. 裝飾者,加了糖霜的飲料DrinkWithSugar,該類不關心把糖霜加在奶茶還是咖啡上,只負責把糖霜載入飲料上。
  4. DrinkWithSugar包含一個Drink屬性,在建構函式中,就設定傳入的drink物件。
  5. DrinkWithSugar繼承自Drink父類,即使加了糖霜之後,它也還是一杯飲料。
  6. DrinkWithSugar重寫getDesc和getCost方法,分別加上“加糖霜(木糖醇)”字樣,和加上糖霜的價格。
// 飲料基類
public abstract class Drink {

    public abstract String getDesc();

    public abstract double getCost();
}

// 被裝飾者
public class MilkTea extends Drink {

    @Override
    public String getDesc() {
        return "奶茶";
    }

    @Override
    public double getCost() {
        return 10;
    }
}

// 裝飾者類
public class DrinkWithSugar extends Drink {

    private Drink drink;

    public DrinkWithSugar(Drink drink) {
        this.drink = drink;
    }

    @Override
    public String getDesc() {
        return drink.getDesc() + "加糖霜(木糖醇)";
    }

    @Override
    public double getCost() {
        return drink.getCost() + 2;
    }
}

// 測試
public class Client {
    public static void main(String[] args) {
        Drink drinkWithSugar = new DrinkWithSugar(new MilkTea());
        System.out.println(drinkWithSugar.getDesc());
        System.out.println(drinkWithSugar.getCost());
    }
}

輸入/輸出:

奶茶加糖霜(木糖醇)
12.0

用上裝飾者模式之後,整個系統不需要修改,只需要新增新的類。

在需要增加新的飲料時,只需新增一個新的飲料類,如Milk。

在需要增加新的加料時,也只需要加一個新的加料類,如DrinkWithSugarFree,用於做加糖免費活動。

在快速的新品,活動迭代下,隔壁王叔叔奶茶店倒閉了。

作為富二代的你看了很高興,準備好好獎勵下小菜。

奶茶店小妹也向程式設計師投來了崇拜的眼神,上來就是一頓酥麻酥麻的誇獎。

然而,第二天,程式設計師就看到奶茶店小妹出現在了富二代的跑車裡。

2405011-b6d441db6656bfff.jpeg
當然是選擇原諒他了


三:再理解

  1. 裝飾者模式是對繼承的再思考。
  2. 加料之後,還是一杯飲料,被裝飾者和裝飾者都繼承自同一個父類。
  3. 對修改關閉,對新增開放,符合設計原則,新增新功能只需增加少量程式碼。
  4. 避免了大量的if else程式碼,邏輯清晰。

相關文章