1. 程式人生 > >裝飾者模式,從吃黃燜雞開始說起

裝飾者模式,從吃黃燜雞開始說起

黃燜雞米飯最熱賣的外賣之一,國人都喜歡吃,吃過黃燜雞米飯的應該都知道,除了黃燜雞米飯主體外,還可以新增各種配菜,如土豆、香菇、鵪鶉蛋、青菜等。如果需要你來設計一套黃燜雞米飯結賬系統,你該如何設計呢?

前置條件:主體:黃燜雞米飯 價格:16,配菜:土豆 價格:2、香菇 價格:2、鵪鶉蛋 價格:2、青菜 價格:1.5

這還不簡單?看我的,你隨手就來了下面這段程式碼。

public class HuangMenJiMiFan {
    // 黃燜雞價格
    private double huangMenJiPrice = 16D;
    // 土豆價格
    private double potatoPrice = 2D;
    // 鵪鶉蛋價格
    private double eggPrice = 2D;
    // 香菇價格
    private double mushroomPrice = 2D;
    // 青菜價格
    private double vegPrice = 1.5D;
    // 總價格
    private double totalPrice = 0D;
    // 訂單描述
    private StringBuilder desc = new StringBuilder("黃燜雞米飯 ");

    // 是否加土豆
    private boolean hasPotato = false;
    // 是否加鵪鶉蛋
    private boolean hasEgg = false;
    // 是否加香菇
    private boolean hasMushroom = false;
    // 是否加蔬菜
    private boolean hasVeg = false;

    public HuangMenJiMiFan(){
        this.totalPrice = this.huangMenJiPrice;
    }

    public void setHasPotato(boolean hasPotato) {
        this.hasPotato = hasPotato;
    }

    public void setHasEgg(boolean hasEgg) {
        this.hasEgg = hasEgg;
    }

    public void setHasMushroom(boolean hasMushroom) {
        this.hasMushroom = hasMushroom;
    }

    public void setHasVeg(boolean hasVeg) {
        this.hasVeg = hasVeg;
    }

    public String getDesc(){
        if (hasEgg){
            this.desc.append("+ 一份鵪鶉蛋 ");
        }
        if (hasMushroom){
            this.desc.append("+ 一份香菇 ");
        }
        if (hasPotato){
            this.desc.append("+ 一份土豆 ");
        }
        if (hasVeg){
            this.desc.append("+ 一份蔬菜 ");
        }
        return desc.toString();
    }

    public double cost(){
        if (hasEgg){
            this.totalPrice +=this.eggPrice;
        }
        if (hasMushroom){
            this.totalPrice +=this.mushroomPrice;
        }
        if (hasPotato){
            this.totalPrice +=this.potatoPrice;
        }
        if (hasVeg){
            this.totalPrice +=this.vegPrice;
        }
        return totalPrice;
    }
}

只要在點黃燜雞米飯的時候,把新增的配菜設定成true就好,這段程式碼確實解決了黃燜雞米飯結算問題。但是我需要加兩份土豆呢?我需要新增一種新配菜呢?或者我新增一個黃燜排骨呢?這時候實現起來就需要去改動原來的程式碼,這違背了設計模式的開放-關閉原則

開放-關閉原則:類應該對擴充套件開放,對修改關閉

上面的設計違背了開放-關閉原則,為了避免這個問題,採用裝飾者模式似乎是一種可行的解決辦法。

裝飾者模式:動態的給一個物件新增一些額外的職責,就增加功能來說,裝飾模式比生成子類更為靈活。

裝飾者模式的通用類圖如下:

從類圖中,我們可以看出裝飾者模式有四種角色:

  • Component:核心抽象類,裝飾者和被裝飾者都需要繼承這個抽象類
  • ConcreteComponent:對裝飾的物件,該類必須繼承Component
  • Decorator:裝飾者抽象類,抽象出具體裝飾者需要裝飾的介面
  • ConcreteDecorator:具體的裝飾者,該類必須繼承Decorator類,並且裡面有一個變數指向Component抽象類

裝飾者模式的核心概念我們都知道了,那就來實現一把,用裝飾者模式來設計黃燜雞米飯的結賬系統。

Component類的設計,仔細想想,不管黃燜雞米飯還是配菜都會涉及到金額計算。所以我們把該方法抽象到Component類。來設計我們黃燜雞米飯結賬系統的Component類,我們取名叫做Food,Food類的具體設計如下:

/**
 * 核心抽象類
 */
public abstract class Food {

    String desc = "食物描述";

    public String getDesc() {
        return this.desc;
    }
    // 價格計算
    public abstract double cost();
}

ConcreteComponent類是我們具體的被裝飾物件,我們這裡的裝飾物件是黃燜雞米飯,我們來設計我們黃燜雞米飯的被裝飾物件Rice類,Rice類的具體實現如下:

/**
 * 被裝飾者-黃燜雞米飯
 */
public class Rice extends Food{
    public Rice(){
        this.desc ="黃燜雞米飯";
    }
    @Override
    public double cost() {
        // 黃燜雞米飯的價格
        return 16D;
    }
}

Decorator類是裝飾者的抽象類,我們需要定義一個getDesc()的抽象介面,因為在Food類中,getDesc()不是抽象的,在後面的具體裝飾者中,需要重寫getDesc()類,所以我們需要將抽象在裝飾者這一層。我們來設計黃燜雞米飯結賬系統的裝飾者抽象類FoodDecoratorFoodDecorator類的具體設計如下:

public abstract class FoodDecorator extends Food {
    // 獲取描述
    public abstract String getDesc();
}

ConcreteDecorator類是具體的裝飾者,我們有四個具體的裝飾者,分別是土豆、香菇、鵪鶉蛋、青菜,具體的裝飾者需要做的事情是計算出被裝飾者裝飾完裝飾品後的總價格和更新商品的描述。四個具體裝飾者的設計如下:

public class Egg extends FoodDecorator {
    String desc = "雞蛋";
    // 存放Component物件,該物件可能是被裝飾後的
    Food food;

    public Egg(Food food){
        this.food = food;
    }

    // 計算總價 當前Component物件的價格加上當前裝飾者的價格
    @Override
    public double cost() {
        return food.cost() + 2D;
    }
    @Override
    public String getDesc() {
        return food.getDesc()+" + "+this.desc;
    }
}
public class Mushroom extends FoodDecorator {
    String desc = "香菇";
    Food food;

    public Mushroom(Food food){
        this.food = food;
    }
    // 計算總價
    @Override
    public double cost() {
        return food.cost() + 2D;
    }
    @Override
    public String getDesc() {
        return food.getDesc()+" + "+this.desc;
    }
}
public class Potato extends FoodDecorator {
    String desc = "土豆";
    Food food;

    public Potato(Food food){
        this.food = food;
    }
    // 計算總價
    @Override
    public double cost() {
        return food.cost() + 2D;
    }
    @Override
    public String getDesc() {
        return food.getDesc()+" + "+this.desc;
    }
}
public class Veg extends FoodDecorator {
    String desc = "蔬菜";
    Food food;
    public Veg(Food food){
        this.food = food;
    }
    // 計算總價
    @Override
    public double cost() {
        return food.cost() + 1.5D;
    }
    @Override
    public String getDesc() {
        return food.getDesc()+" + "+this.desc;
    }
}

裝飾者的所有角色都實現完了,我們來測試一下使用裝飾者模式之後的黃燜雞結賬系統,編寫一個App測試類。

public class App {
    public static void main(String[] args) {
        // 點一份米飯
        Rice rice = new Rice();
        // 加個雞蛋
        Egg egg = new Egg(rice);
        // 在加土豆
        Potato potato = new Potato(egg);
        // 再加一份白菜
        Veg veg = new Veg(potato);
        System.out.println(veg.getDesc());
        System.out.println(veg.cost());
    }
}

測試結果

我們的描述和金額都是正確的,可能你還是沒怎麼明白裝飾者模式,一起來看看我們的黃燜雞米飯被裝飾後的示意圖:


我們的黃燜雞米飯共有三層裝飾,第一層是雞蛋,第二層是土豆,第三層是蔬菜。我們在最後呼叫價格計算和商品描述都是呼叫了最外層的裝飾者的方法,有點像遞迴一樣,每一層的裝飾者都有被前一個裝飾者裝飾後的黃燜雞米飯物件。裡面會產生想遞迴一樣的呼叫。希望看完這張圖之後,對你理解裝飾者模式有幫助。

使用裝飾者模式之後的黃燜雞米飯結賬系統,在新增配菜或者產品時,我們不需要修改原先的功能,只需要對類進行擴充套件就好了,這完全遵循了開放-關閉原則

裝飾者模式的優點

  • 裝飾類和被裝飾類可以獨立發展,而不會互相耦合,換句話說,就是Component類無須知道Decorator類,Decorator類也不用知道具體的被裝飾者。
  • 裝飾者模式是繼承關係的一個替代方案,從上面的黃燜雞米飯的案例中,我們可以看出,不管裝飾多少層,返回的物件還是Component
  • 裝飾者模式可以動態的擴充套件一個實現類的功能

裝飾者模式的優點

  • 多層裝飾模式比較複雜,你可以想象一下剝洋蔥,如果最裡面的裝飾出了問題,你的工作量會有多大?

最後多說一句,JDK 中的 java.io 就是用裝飾者模式實現的,有興趣的可以去深入瞭解一下。

原始碼

文章不足之處,望大家多多指點,共同學習,共同進步

最後

打個小廣告,歡迎掃碼關注微信公眾號:「平頭哥的技術博文」,一起進步吧。