裝飾者模式-動態的包裝原有物件的行為

碼農充電站發表於2020-12-30

公號:碼農充電站pro
主頁:https://codeshellme.github.io

今天來介紹裝飾者模式Decorator Design Pattern)。

假設我們需要給一家火鍋店設計一套結賬系統,也就是統計顧客消費的總價格。怎樣才能設計出一個好的系統呢?

1,結賬系統需求分析

既然要設計一個結賬系統,當然需要知道火鍋店都有哪些食品及食品的價格,假如我們從火鍋店老闆那裡得到以下食品清單:

  • 鍋底類:
    • 清湯鍋底:5 元
    • 麻辣鍋底:7 元
    • 其它
  • 配菜類:
    • 青菜:3 元
    • 羊肉:6 元
    • 其它
  • 飲料類:
    • 可樂:2 元
    • 其它

可以看到,食品共有三大類,分別是:鍋底類,配菜類和飲料類,每個大類下邊都有很多具體的食品。

為了設計出一個可維護,可擴充套件,有彈性的系統,應該怎樣設計呢?

我們可以這樣看待食品之間的關係,將鍋底類看作主品,所有其它的都為副品,也就是附加在主品之上的食品。

副品以主品為中心,圍繞在主品周圍,包裹著主品,一層層的往外擴充套件

如下圖所表達的一樣:

在這裡插入圖片描述

2,裝飾者模式

像這種,需要在原來(主品)的基礎上,附加其它的東西(副品),這樣的業務場景都可以使用裝飾者模式

裝飾者模式的定義為:動態的給一個物件新增其它功能。從擴充套件性來說,這種方式比繼承更有彈性,更加靈活,可作為替代繼承的方案

裝飾者模式的優點在於,它能夠更靈活的,動態的給物件新增其它功能,而不需要修改任何現有的底層程式碼。也就是不需要通過修改程式碼,而是通過擴充套件程式碼,來完成新的業務需求。

這就非常符合我們所說的設計原則中的開閉原則對擴充套件開放,對修改關閉。也就是儘量不要修改原有程式碼,而是通過擴充套件程式碼來完成任務。這樣做的好處是可以減少對原有系統的修改,從而減少引入 bug 的風險。

裝飾者模式的類圖如下:

在這裡插入圖片描述

ConcreteComponent 為被裝飾者,Decorator 是所有裝飾者的超類。

裝飾者和被裝飾者有著共同的超型別,這一點很重要,因為裝飾者必須能夠取代被裝飾者。這樣,裝飾者才能在被裝飾者的基礎上,加上自己的行為,以增強被裝飾者的能力。

一個被裝飾者可以被多個裝飾者依次包裝,這個包裝行為是動態的,不限次數的。

3,實現結賬系統

那麼根據裝飾者模式的類圖,我們可以設計出火鍋店結賬系統的類圖,如下:

在這裡插入圖片描述

火鍋的鍋底作為被裝飾者,配菜和飲料作為裝飾者。

每個類都有兩個方法:

  • describe:返回當前火鍋的描述。
  • cost:返回當前火鍋的價格。

首先編寫 HotPot 類:

class HotPot {
    protected String desc = "HotPot";
    protected double price = 0;

    public String description() {
        return desc;
    }

    public double cost() {
        return price;
    }
 
    public void printMenu() {
        System.out.println("選單:" + description() + " 消費總價:" + cost());
    }
}

HotPot 類中有兩個屬性 descprice,還有三個方法 descriptioncostprintMenuprintMenu 用於輸出選單和消費總價。

再編寫 SideDish 類:

class SideDish extends HotPot {
    protected HotPot hotpot;
    
    public double cost() {
        return hotpot.cost() + price;
    };

    public String description() {
        return hotpot.description() +" + "+ desc;
    };
}

SideDish 繼承了 HotPot,新增了自己的屬性 hotpot,並且重寫了兩個方法 descriptioncost

注意SideDish 類對兩個方法 descriptioncost 進行了重寫,這非常重要,這體現出了裝飾者與被裝飾者的區別,裝飾者能在被裝飾者的基礎上附加自己的行為,原因就在這裡

編寫兩個鍋底類:

class SoupPot extends HotPot {
    public SoupPot() {
        desc = "Soup";
        price = 5;
    }
}

class SpicyPot extends HotPot {
    public SpicyPot() {
        desc = "Spicy";
        price = 7;
    }
}

這兩個類都繼承HotPot,並分別在構造方法中設定自己的 descprice

再編寫三個配菜類:

class VegetablesDish extends SideDish {
    public VegetablesDish(HotPot hotpot) {
        this.hotpot = hotpot;
        desc = "Vegetables";
        price = 3;
    }
}

class MuttonDish extends SideDish {
    public MuttonDish(HotPot hotpot) {
        this.hotpot = hotpot;
        desc = "Mutton";
        price = 6;
    }
}

class ColaDish extends SideDish {
    public ColaDish(HotPot hotpot) {
        this.hotpot = hotpot;
        desc = "Cola";
        price = 2;
    }
}

這三個類都繼承 SideDish,並分別在構造方法中設定自己的hotpotdescprice

4,測試結賬系統

用如下程式碼來測試:

// 只有一份清湯鍋底
HotPot hotpot = new SoupPot(); // 被裝飾者不需裝飾者包裝也可以使用
hotpot.printMenu();

// 清湯鍋底 + 蔬菜
hotpot = new VegetablesDish(hotpot);
hotpot.printMenu();

// 清湯鍋底 + 蔬菜 + 羊肉
hotpot = new MuttonDish(hotpot);
hotpot.printMenu();

// 清湯鍋底 + 蔬菜 + 羊肉 + 可樂
hotpot = new ColaDish(hotpot);
hotpot.printMenu();

// 清湯鍋底 + 蔬菜 + 羊肉 + 可樂 + 蔬菜
hotpot = new VegetablesDish(hotpot);
hotpot.printMenu();

輸出如下:

選單:Soup 消費總價:5.0
選單:Soup + Vegetables 消費總價:8.0
選單:Soup + Vegetables + Mutton 消費總價:14.0
選單:Soup + Vegetables + Mutton + Cola 消費總價:16.0
選單:Soup + Vegetables + Mutton + Cola + Vegetables 消費總價:19.0

計算總價時,會從最外層的裝飾者,朝著被裝飾者的方向,依次呼叫每一層的 cost 方法,直到被裝飾者為止。

然後再朝著最外層裝飾者的方向,依次計算出每一層的價格,最後得出的價格就是消費總價。

計算過程如下圖所示:

在這裡插入圖片描述

我將完整的裝飾者模式程式碼放在了這裡,供大家參考。

5,裝飾者模式的使用場景

裝飾者模式主要用於,在不修改原有類的前提下,動態的修改原有類的功能。

Java JDK 中大量使用了裝飾者模式,尤其是 Java I/O 框架。

Java IO 框架的繼承關係如下:

在這裡插入圖片描述

可以看到,Java IO 框架包含了非常多的類,這對初學者並不是很友好,很難弄明白每個類的作用是什麼,也不容易瞭解設計者的意圖。

Java IO 主要分為位元組流字元流兩大類。我們以 InputStream 為例,畫出其類圖結構,如下:

在這裡插入圖片描述

從該圖能夠看出,Java IO 與我們上文中的裝飾者模式的類圖基本一模一樣,所以 Java IO 其實就是使用了裝飾者模式,明白了這一點,再使用它就非常方便了。

6,裝飾者模式的缺點

裝飾者模式有一個比較明顯的缺點,從上文中你也許已經發現了,就是它會引入非常多的小類,這樣會讓使用者弄不明白類之間的關係。

當了解了裝飾者的原理,也就比較容易使用了。

7,總結

從裝飾者模式中,能充分的看到開閉原則的使用。利用裝飾者模式,可以讓我們在不修改原有程式碼的情況下,擴充套件原有類的功能。但是也不能過度使用它,因為容易引入非常多的小類。

(本節完。)


推薦閱讀:

設計模式之高質量程式碼

單例模式-讓一個類只有一個物件

工廠模式-將物件的建立封裝起來

策略模式-定義一個演算法族

觀察者模式-將訊息通知給觀察者


歡迎關注作者公眾號,獲取更多技術乾貨。

碼農充電站pro

相關文章