【阿里大神講設計模式】5.可樂要加冰才好喝啊---裝飾模式

前情提要

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

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

  1. 可樂能不能加冰啊
  1. 綠豆湯加點糖唄
    ......

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

改進之路

第一套方案

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

小光的第一套方案:

然而, 還沒有投入使用呢, 小光就發(fā)現(xiàn)了問題:

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

裝飾出場

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

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

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

良好的面向接口編程習慣, 小光抽象了一個飲料類Drink

public interface Drink {
    String make();
}

實現(xiàn)一杯可樂:

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

加了冰塊的飲料還應該是飲料, 故而, 冰塊這個裝飾也實現(xiàn)了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());
    }
}

結(jié)果:

這是一杯可樂
這是一杯可樂, 加一塊冰

持續(xù)改進

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

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

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 "冰";
    }
}

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

具體代碼就不在此貼出來了, 全部代碼在這里.

投入使用

經(jīng)過大量驗證后, 小光將新的程序投入了使用, 現(xiàn)在是這樣子的...

"老板, 來一杯可樂, 加冰"

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());
這是一杯可樂, 加一份蜂蜜, 加一份冰, 加一份冰

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

故事之后

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

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

我們的Stuff(料)也是實現(xiàn)了Drink接口的, 這是為了說明加了料(Stuff)的飲料還是飲料.
Stuff中還聚合了一個Drink(originalDrink)實例, 是為了說明這個料是加到飲料中的.
對, 這個就是一個標準的裝飾者模式的類圖.

裝飾者模式就是用來動態(tài)的給對象加上額外的職責.
Drink是被裝飾的對象, Stuff作為裝飾類, 可以動態(tài)地給被裝飾對象添加特征, 職責.

擴展閱讀一

有的同學可能會說, 我完全可以通過繼承關系, 在子類中添加職責的方式給父類以擴展啊. 是的, 沒錯, 繼承本就是為了擴展.

然而, 裝飾者模式和子類繼承擴展的最大區(qū)別在于:

裝飾者模式強調(diào)的是動態(tài)的擴展, 而繼承關系是靜態(tài)的.

由于繼承機制的靜態(tài)性, 我們會為每個擴展職責創(chuàng)建一個子類, 例如IceCoke, DoubleIceCoke, SugarXDrink, IceSugarXDrink等等...會造成類爆炸.

另外, 這里引入一條新的面向?qū)ο缶幊淘瓌t:
組合優(yōu)于繼承, 大家自行體會下.

擴展閱讀二

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

這里先說明下二者的本質(zhì)差異:

Builder模式是一種創(chuàng)建型的設計模式. 旨在解決對象的差異化構(gòu)建的問題.
裝飾者模式是一種結(jié)構(gòu)型的設計模式. 旨在處理對象和類的組合關系.

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

Builder模式的差異化構(gòu)建是可預見的, 而裝飾者模式實際上提供了一種不可預見的擴展組合關系.

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

XDrink xDrink = new XDrink.Builder()
        .withHoney()
        .build()

但是如果飲料拿出來后, 用戶還想加冰怎么辦呢? 這就是Builder模式在這種動態(tài)擴展情景下的局限. 看看裝飾模式怎么做:

// 加蜂蜜的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)對象. 個中好處大家自行體會下.

擴展閱讀三

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

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

Android中鼎鼎大名的Context體系, 實際上就是裝飾模式的體現(xiàn), 通過裝飾模式來給Context擴展了一系列的功能. 如下:

還是比較清晰的, 典型的使用裝飾者模式來擴展功能的實踐, 相比于我們的例子, 更注重功能擴展, 體現(xiàn)了開閉原則.

再重申下, 面向?qū)ο缶幊淌且环N思想, 設計模式都是這些思想的具體實踐和體現(xiàn). 學習設計模式可以讓我們更好的理解編程思想, 而非固定套用. 我們的目的是無招勝有招.

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

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

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容