装饰者模式,从吃黄焖鸡开始说起

  • 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 就是用装饰者模式实现的,有兴趣的可以去深入了解一下。

源代码

文章不足之处,望大家多多指点,共同学习,共同进步

最后

打个小广告,欢迎扫码关注微信公众号:「平头哥的技术博文」,一起进步吧。
平头哥的技术博文