装饰者模式

背景
  1. 星巴兹是以扩张速度最快而闻名的咖啡连锁店。因为扩张速度实在太快,他们准备更新订单系统,以合乎他们的饮料供应需求。他们原先的类设计是这样的:Beverage(饮料)是一个抽象类,店内所提供的饮料必须继承自此类,例如深焙,综合,低咖啡因,浓缩等咖啡。抽象类包含cost抽象方法,用于子类实现;包含description属性,用于描述各饮料名称。

    1
    2
    3
    4
    5
    public abstract class Beverage{
    protected String description;
    public String getDescription(){return description}
    public abstract float cost();
    }

    当然在买咖啡的时候,还可以加入各种调料,例如蒸奶,豆浆等。星巴兹会根据不同的调料,收取不同的价格。

    这就导致了类爆炸,什么深焙蒸奶,深焙豆浆,浓缩豆浆咖啡等组合,都是通过cost来得到价格,组合只会随着咖啡的种类和配料的种类越来越多,维护起来简直就是噩梦。

    1
    2
    3
    public HouseBlendWithSteamedMilkandMocha extend Beverage ...
    public DarkRoastWithSteamedMilkandCaramel extend Beverage ...
    //各种组合类
  2. 不使用类进行价格的计算,而改用实例变量和继承的方式实现。

    Beverage基类,加上实例变量代表是否加上调料(牛奶、豆浆、摩卡、奶泡…)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public abstract class Beverage{
    protected String description;
    //蒸奶
    private boolean milk;
    //豆浆
    private boolean soy;
    //摩卡
    private boolean mocha;
    //奶泡
    private boolean whip;
    public String getDescription(){return description}
    //cost提供实现,但是子类仍然需要调用超类,计算出基本饮料加上调料的价钱。
    public float cost(){
    float condimentCost = 0;
    if(hasMilk()){
    return condimentCost+0.9f
    }
    if(hasSoy()){
    return condimentCost+1.2f
    }
    ...
    //各种调料
    return condimentCost;
    }
    public boolean hasMilk(){
    return milk;
    }
    public void setMilk(boolean flag){
    milk = flag;
    }
    public boolean soy
    ...
    //取得和设置调料的布尔值
    }

    现在加入子类,每个类代表菜单上的一种饮料:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class DarkRoast extends Beverage{
    public DarkRoast(){
    description="Most Excellent Dark Roast"
    }
    public float cost(){
    return 0.2f + super.cost();
    }

    }

    那么有哪些需求会影响这个设计呢?

    • 调料价钱的改变会是我们更改现有代码
    • 一旦出现新的调料,我们就需要加上新的方法,并改变超类中的cost()方法
    • 出现一种不需要这些配料的饮料会造成方法的冗余
    • 顾客想要双倍摩卡咖啡,会很麻烦
设计原则

类应该对扩展开放,对修改关闭。

我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。如能实现这样的目标,有什么好处呢?这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求。

虽然似乎有点矛盾,但是的确有一些技术可以允许在不直接修改代码的情况下对其进行扩展。

在选择需要被扩展的代码部时要小心。每个地方都采用开放-关闭原则,是一种浪费,也没必要,还会导致代码变得复杂且难以理解。

装饰者模式

动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

在理解到继承无法完全解决问题,所以我们采用:以饮料为主体,然后在运行时以调料来“装饰”饮料。比如说,如果顾客想要摩卡和奶泡深焙咖啡,那么我们要做的是:

  • 拿一个深焙咖啡(DarkRoast)对象
  • 以摩卡(Mocha)对象装饰它
  • 以奶泡(Whip)对象装饰它
  • 调用cost()方法,并依赖委托(delegate)将调料的价钱加上去

下面我们将使用装饰者模式,实现星巴兹饮料

  • 每个组件可以单独使用,或者被装饰者包装起来使用。
1
2
3
4
5
6
7
8
//基础类 相当于抽象的Component类(装饰类和被装饰类的基本类型);
public abstract class Beverage{
protected String descripton = "Unknown Beverage";
public String getDescrition(){
return description;
}
public abstract float cost();
}
  • 装饰者共同实现的接口(也可以是抽象类)
1
2
3
4
//Condiment(调料)抽象的Decorator类(装饰类):
public abstract class CondimentDecorator extends Beverage{
public abstract String getDescription();
}
  • 每个装饰者都“有一个”(包装一个)组件,也就是说,装饰者有一个实例变量以保存某一个Component的引用;
1
2
3
4
5
6
7
8
9
10
//具体的饮料代码 深焙
public class HouseBlend extends Beverage{
public HouseBlend(){
description = "House Blend Coffee";
}
public float cost(){
return 0.89f;
}
}
//依次实现 综合,深焙,低咖啡因,浓缩等饮料类。
  • 写调料代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//摩卡是一个装饰者,所以让他扩展自CondimentDecorator。
public class Mocha extends CondimentDecorator{
//让Mocha能够引用一个Beverage,
Beverage beverage;
public Mocha(Beverage beverage){
this.beverage = beverage;
}
public String getDescription(){
return beverage.getDescritpion() + ", Mocha";
}
public float cost(){
//计算带Mocha饮料的价钱,首先把调用委托给被装饰对象,以计算价钱,然后再加上Mocha的价钱,得到最后的结果。
return 0.20f+beverage.cost();
}
}
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
public class StarbuzzCoffee{
public static void main(String args[]){
//制造一杯调料为豆浆、摩卡、奶泡的HouseBlend咖啡。
Beverage beverage = new HouseBlend();
beverage = new Soy(beverage);
beverage = new Mocha(beverage);
beverage = new Whip(beverage);
System.out.println(beverage.getDescription()+" $"+beverage.cost());
// House Blend Coffee,Soy,Mocha,Whip $1.34
}
}
总结
  • 其中,CondimentDecorator扩展自Beverage类,用到了继承,但是我们这里利用继承是达到“类型匹配”,而不是利用继承获得“行为”;而行为来自装饰者和基础组件,或与其他装饰者之间的组合关系;当然Beverage和CondimentDecorator也可以是接口。
  • 在装饰的过程会带来类型的改变,如果我将代码针对特定种类的组件(例如House Blend),做一些特殊的事(例如打折),那么这件事就会被受到影响,所以我们还需要考虑应用架构是否需要该模式,以及装饰者是否合适。