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

  • 2019 年 10 月 3 日
  • 筆記

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

前置條件:主體:黃燜雞米飯 價格: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 就是用裝飾者模式實現的,有興趣的可以去深入了解一下。

源代碼

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

最後

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