可樂要加冰才好喝啊 — 裝飾模式

anly_jun發表於2019-02-28

前情提要

上集講到, 小光利用策略模式搞起了回饋顧客的活動. 還別說, 客流量增大不少.

然而, 隨之而來的, 顧客的聲音也不少:

  • 可樂能不能加冰啊
  • 綠豆湯加點糖唄
  • ……

眾口難調嘛, 大家的需求不一, 有的要冰有的不要, 有的加糖有的不要… 小光帶著客戶的意見, 開始了飲品的改進之路.

改進之路

第一套方案

很快, 小光想出了第一套的解決方案: 我把加冰和不加冰的的飲料看成是兩種不同的飲料, 藉助上次設計的工廠方法模式的飲料機, 可以很輕易的擴充套件實現使用者不同需求, 而不修改原來的實現啊.

小光的第一套方案:

可樂要加冰才好喝啊 — 裝飾模式

然而, 還沒有投入使用呢, 小光就發現了問題:

我這不定期會從飲料商戶那兒拿到一些不同的飲品, 每種都有可能要加冰, 加糖, 還可能加蜂蜜呢…那我不用幹別的啦, 天天折騰這些飲料機就夠夠的啦.

裝飾出場

那麼有什麼更好的辦法呢? 小光又陷入了冥想.
這時, 表妹過來了, 興高采烈的, 看到小光傻傻的樣子, 問: “表哥, 你想啥呢, 看看我剛新買的頭花好看不?”
小光: “你怎麼又換頭花了啊?”
表妹: “女孩子嘛, 當然要換著花樣打扮裝飾自己啊, 好不好看嘛?”
“好看好看.” 小光敷衍答道.

突然, 小光像是想起了什麼, 喃喃自語道, “打扮”, “裝飾”, “對啊, 我也可以用冰啊, 糖啊, 蜂蜜什麼的來裝飾我的飲料啊.”

說做就做, 小光先從可樂入手:

良好的面向介面程式設計習慣, 小光抽象了一個飲料類Drink

public interface Drink {

    String make();
}複製程式碼

實現一杯可樂:

public class Coke implements Drink {
    @Override
    public String make() {
        return "這是一杯可樂";
    }
}複製程式碼

加了冰塊的飲料還應該是飲料, 故而, 冰塊這個裝飾也實現了Drink介面, 加冰這個裝飾是在飲料的基礎上的, 所以我們還持有了一個Drink物件:

public class Ice implements Drink {

    private Drink originalDrink;
    public Ice(Drink originalDrink) {
        this.originalDrink = originalDrink;
    }

    @Override
    public String make() {
        return originalDrink.make() + ", 加一塊冰";
    }
}複製程式碼

來實驗下:

public class XiaoGuang {

    public static void main(String[] args) {

        Drink coke = new Coke();
        System.out.println(coke.make());

        Drink iceCoke = new Ice(new Coke());
        System.out.println(iceCoke.make());
    }
}複製程式碼

結果:

這是一杯可樂
這是一杯可樂, 加一塊冰複製程式碼

持續改進

小光看到實驗結果, 哈哈大笑, 實驗成功, 且看我推廣到所有飲料, 所有加料中.

小光想到, 加料可能是糖, 可能是冰, 還可能蜂蜜, 還有很多未知的, 為了滿足開閉原則, 我還是先抽象一個配料出來吧:

public abstract class Stuff implements Drink {

    private Drink originalDrink;
    public Stuff(Drink originalDrink) {
        this.originalDrink = originalDrink;
    }

    @Override
    public String make() {
        return originalDrink.make() + ", 加一份" + stuffName();
    }

    abstract String stuffName();
}複製程式碼

冰塊原料改為繼承Stuff:

public class Ice extends Stuff {

    public Ice(Drink originalDrink) {
        super(originalDrink);
    }

    @Override
    String stuffName() {
        return "冰";
    }
}複製程式碼

小光還據此增加了糖Sugar, 和蜂蜜Honey原料.

具體程式碼就不在此貼出來了, 全部程式碼在這裡.

投入使用

經過大量驗證後, 小光將新的程式投入了使用, 現在是這樣子的…

“老闆, 來一杯可樂, 加冰”

Drink iceCoke = new Ice(new Coke());
System.out.println(iceCoke.make());

這是一杯可樂, 加一份冰複製程式碼

“老闆, X飲料, 加冰, 加糖”

Drink iceSugarXDrink = new Ice(new Sugar(new XDrink()));
System.out.println(iceSugarXDrink.make());

這是一杯X牌飲料, 加一份糖, 加一份冰複製程式碼

“可樂, 加兩份冰, 加蜂蜜”

Drink doubleIceHoneyCoke = new Ice(new Ice(new Honey(new Coke())));
System.out.println(doubleIceHoneyCoke.make());

這是一杯可樂, 加一份蜂蜜, 加一份冰, 加一份冰複製程式碼

完美表現.
小光再也不怕顧客們的各種奇怪的加料要求了.

故事之後

照例, 故事之後, 我們用UML類圖來梳理下上述的關係:

可樂要加冰才好喝啊 — 裝飾模式

其中Stuff類中值得注意的兩個關係:

  1. 我們的Stuff(料)也是實現了Drink介面的, 這是為了說明加了料(Stuff)的飲料還是飲料.
  2. Stuff中還聚合了一個Drink(originalDrink)例項, 是為了說明這個料是加到飲料中的.

對, 這個就是一個標準的裝飾者模式的類圖.

裝飾者模式就是用來動態的給物件加上額外的職責.
Drink是被裝飾的物件, Stuff作為裝飾類, 可以動態地給被裝飾物件新增特徵, 職責.

擴充套件閱讀一

有的同學可能會說, 我完全可以通過繼承關係, 在子類中新增職責的方式給父類以擴充套件啊. 是的, 沒錯, 繼承本就是為了擴充套件.

然而, 裝飾者模式和子類繼承擴充套件的最大區別在於:

裝飾者模式強調的是動態的擴充套件, 而繼承關係是靜態的.

由於繼承機制的靜態性, 我們會為每個擴充套件職責建立一個子類, 例如IceCoke, DoubleIceCoke, SugarXDrink, IceSugarXDrink等等…會造成類爆炸.

另外, 這裡引入一條新的物件導向程式設計原則:
組合優於繼承, 大家自行體會下.

擴充套件閱讀二

還有的同學說, 這種按需定製的方式貌似跟之前講的Builder模式有點像啊, 那為什麼不用Builder模式呢.

這裡先說明下二者的本質差異:

Builder模式是一種建立型的設計模式. 旨在解決物件的差異化構建的問題.
裝飾者模式是一種結構型的設計模式. 旨在處理物件和類的組合關係.

實際上在這個例子中, 我們是可以用Builder模式的, 但就像使用繼承機制一樣, 會有些問題.
首先, Builder模式是構建物件, 那麼實際上要求我們是必須事先了解有哪些屬性/職責供選擇. 這樣我們才可以在構建物件時選擇不同的Build方式. 也就是說:

Builder模式的差異化構建可預見的, 而裝飾者模式實際上提供了一種不可預見的擴充套件組合關係.

例如, 如果使用者要了一杯帶蜂蜜的X飲料, 如果是Builder模式, 我們可能是這樣的:

XDrink xDrink = new XDrink.Builder()
        .withHoney()
        .build()複製程式碼

但是如果飲料拿出來後, 使用者還想加冰怎麼辦呢? 這就是Builder模式在這種動態擴充套件情景下的侷限. 看看裝飾模式怎麼做:

// 加蜂蜜的X飲料
Drink honeyXDrink = new Honey(new XDrink());
System.out.println(honeyXDrink.make());

這是一杯X牌飲料, 加一份蜂蜜

// 還要加冰
Drink iceHoneyXDrink = new Ice(honeyXDrink);
System.out.println(iceHoneyXDrink.make());

這是一杯X牌飲料, 加一份蜂蜜, 加一份冰複製程式碼

直接在honeyXDrink的基礎上再裝飾一份冰即可.

另外, 大家也都看到了, 由於我們的面向介面程式設計方式, 裝飾者(冰塊, 蜂蜜, 糖)可不是隻能用來裝飾特定的被裝飾物件(諸如可樂), 它們可以被用來裝飾所有種類的飲料(Drink)物件. 箇中好處大家自行體會下.

擴充套件閱讀三

我們一直在說的, 裝飾者模式是動態給物件加上額外的職責, 這個職責實際上包括修飾(屬性), 也包括行為(方法).

例如我們上面討論的例子, 就是單純得給飲料加上了一些修飾, 是飲料程式設計了加冰的飲料, 加糖的飲料. 我們也可以給物件加上行為, 例如, 一部手機, 我們可以通過裝飾模式給它加上紅外遙控的功能, 還可以給他加上NFC支付的功能.

Android中鼎鼎大名的Context體系, 實際上就是裝飾模式的體現, 通過裝飾模式來給Context擴充套件了一系列的功能. 如下:

可樂要加冰才好喝啊 — 裝飾模式

還是比較清晰的, 典型的使用裝飾者模式來擴充套件功能的實踐, 相比於我們的例子, 更注重功能擴充套件, 體現了開閉原則.

再重申下, 物件導向程式設計是一種思想, 設計模式都是這些思想的具體實踐和體現. 學習設計模式可以讓我們更好的理解程式設計思想, 而非固定套用. 我們的目的是無招勝有招.

另外, 具體的環境中設計模式一般不是單一地用的, 往往是各種設計模式融合在一起的. 例如我們這個裝飾者裡面, 各種new物件, 實際上就可以用到我們學習的工廠方法的模式進行處理.

好了, 讓我們充分發揮自己的想象力, 多弄些原料來裝飾我們的飲料吧, 說不定我們能調出一種前所未有的飲品呢, 哈哈.

系列文在此: 小光的開店之路–設計模式

相關文章