# 装饰者模式

# 现实案例

  • 现在有一个咖啡店,他们有很多很多不同种类的咖啡/饮料在售卖;
  • 顾客购买咖啡时,可以要求向里面加入各种调料(奶泡,牛奶,砂糖,等...)。根据顾客加的东西,会在咖啡的基础上进行额外收费;
  • 现在咖啡店需要一套 “订单系统” 可以让他们很轻松的计算出每个咖啡的价格;

# 实现一:❌ 用继承来实现:

  • 先创建一个 Beverage 抽象类,店里所有的饮料都要继承自此类;
  • 每种调料(牛奶,砂糖,等...)都各自有一个 int 类型实例属性,它们的属性值代表对应的调料点了多少份;
  • 同时,每种调料的实例属性,还有对应的 getter / setter 方法;
  • Beverage 里面有个方法 cost,它会计算出咖啡的价格:
    • 我们默认,调料价钱计算方法是通用的;
    • 现在 Beverage 的 cost 方法里算出调料的价格;
    • 在每个饮料子类中,我们需要重写覆盖这个 cost 方法。在子类的 cost 方法中,先调用父类的 cost 算出调料的价格,然后再加上对应饮料的价格;

2020-2-18-19-31-29.png

// 超类
public abstract class Beverage {
    protected String description;
    protected int soy = 0;
    protected int mocha = 0;

    public String getDescription() {
        return this.description;
    }

    // 计算好添加的调味料价格,让子类直接调用
    public float cost() {
        float sum = 0;

        sum += getMocha() * 3.2f;
        sum += getSoy() * 4.6f;

        return sum;
    }

    protected int getMocha() {
        return this.mocha;
    }

    protected void setMocha(int mocha) {
        this.mocha = mocha;
    }

    protected int getSoy() {
        return this.soy;
    }

    protected void setSoy(int soy) {
        this.soy = soy;
    }
}

// 子类
class Coffee extends Beverage {
    @Override
    public float cost() {
        // 价格等于调料价格 + 10 块咖啡钱
        return super.cost() + 10;
    }
}

# 实现一的问题:

  • 无论添加,还是删除调料,我们都要改变 Beverage 抽象类的 cost 方法;
  • 我们假定所有饮料都用同一套调料计算方法,但是如果需求不是这样的呢?我们就要加很多判断语句,或者在子类重写一套计算方法;
  • 并不是每种饮料都可以加所有的调料。比如 “茶” 可能并不需要加奶泡,但是上面的实现,让每种饮料都继承了所有  调料的 setter / getter 方法;

# 什么是装饰者模式

  • 装饰者模式遵循了 “开放-关闭原则”。对扩展开放,就修改关闭。也就是在不修改原有代码的情况下,给类添加新的行为;
  • 装饰者模式动态的将新行为附加到原本的对象上,以此来扩展功能;
  • 相对于继承,装饰者提供了更有弹性的替代方案;

# 装饰者模式的实现

  • 在之前,我们的模式是,一个超类下面有很多个子类。假设这个超类叫 Component,子类叫做 ConcreteComponent;
  • 现在在这个超类下,我们再创建一个装饰超类/接口;
  • 然后再在装饰超类/接口下,实现装饰者类:
    • 装饰者内部有一个 Component 类型实例属性,它可以保存继承自 Component 的子类实例的引用;这里我们把它称作 “被装饰者”;
    • 在实例化装饰类的时候,我们把 Component 的子类实例传进构造函数,并保存到刚刚的实例属性中;
    • 通过对被装饰者的引用,我们可以进行各种操作,给它加上各种行为;
    • 因为装饰者和被装饰者有相同的超类型,所以我们把装饰者看作是对被装饰者的 “包装”。装饰者可以替代被装饰者对象;
  • 通过上面这种方式,我们可以在如何时候,动态地,不限量地来给目标对象加上装饰对象;
  • 并以此来扩展目标对象的行为;

2020-2-18-21-3-29.png

# 实现二:✅ 用装饰者模式实现

  • 通过装饰者模式,我们在运行时用调料来 “装饰” 饮料;
  • 🌰 以一杯 “摩卡奶泡深焙咖啡” 举例:
  • 我们先有一个 Beverage 超类。它里面有一个 cost 抽象方法;
  • 然后创建一个深焙咖啡 DarkRoast 类,让它继承自 Beverage 类。它要实现自己的 cost 方法,里面返回单个咖啡的价格;
  • 创建 Condiment 调料抽象类,它继承自 Beverage 类。作为装饰者的超类,所有的调料装饰者类都继承自它;
    • 创建摩卡 Mocha 装饰类;
    • 创建奶泡 Whip 装饰类;
    • 两个装饰类都包含一个 Beverage 类型的实例属性,它作为 “被装饰者” 的引用;
    • 装饰类里面要实现 cost 方法,它把 “当前调料的价格” 加到 “被装饰对象的价格” 之上,并将结果返回出去;
  • 现在,先创建一个 DarkRoast 实例;
  • 然后创建 Mocha 装饰实例,把 DarkRoast 实例作为 “被装饰者” 传进去;
  • 然后创建 Whip 装饰实例,把前一个 Mocha 实例作为 “被装饰者” 传进去;
  • 现在调用 Whip 实例的 cost 方法就会返回正确的价格;

2020-2-18-23-29-10.png

public abstract class Beverage {
  public abstract double cost();
}

public class DarkRoast extends Beverage {
  public double cost() {
    return 10; // 单杯咖啡的价格 10 块钱;
  }
}
// 调料装饰超类
public abstract class CondimentDecorator extends Beverage {

}

public class Mocha extends CondimentDecorator {
  Beverage beverage;

  public Mocha(Beverage beverage) {
    this.beverage = beverage;
  }

  public double cost() {
    return 1 + beverage.cost();
  }
}

public class Whip extends CondimentDecorator {
  Beverage beverage;

  public Mocha(Beverage beverage) {
    this.beverage = beverage;
  }

  public double cost() {
    return 2 + beverage.cost();
  }
}
Beverage coffee = new DarkRoast();
coffee = new Mocha(coffee);
coffee = new Whip(coffee);
System.out.println(coffee.cost());
// 在 cost 被调用时,价格为 2 + 1 + 10

# 总结:装饰者模式

  • 继承属于扩展形式之一,但不一定是可以达到弹性设计的最佳方案;
  • 在设计时,应该允许对象的行为被扩展,而并不需要改变现有代码
  • 通过组合和委托,可以动态地给对象加上新的行为
  • 装饰者和被装饰者类都继承自同一个超类;他们的实例之间可以互相替换
  • 通过这个特性,装饰者模式得以实现,装饰者可以在被装饰者身上加上新的行为,然后再用自己替代掉被装饰者。通过这种方式,被装饰者的功能得以扩展;
  • 虽然装饰者模式很灵活好用,但也要注意:
    • 装饰者类把所有的被装饰者都看作 “同一个超类型” 的实例。它并不分辨被装饰者的具体子类型。这使得,如果一个行为只针对特别的一个子类型生效,那装饰者就需要再内部做类型判断。可能这种情况下,装饰者模式就不好用了;
    • 装饰者模式会让设计中出现很多个小对象(装饰者类)。如果过度使用,也不宜与维护,也会让程序变得复杂;
上次更新: 8/22/2020, 8:14:09 AM