业务场景:
火锅店有3种火锅底锅:鸳鸯锅,香辣锅,菌菇锅;有4种可加的菜:生菜,豆腐,羊肉,牛肉。如下图所示。顾客可以店一个底锅,配任任意菜(数量和种类都任意),那么顾客可能点的火锅种类就有点多了:鸳鸯锅加生菜加豆腐加羊肉,香辣锅加生菜加羊肉加牛肉,菌菇锅加3份羊肉加5份牛肉(豪),鸳鸯锅加1份生菜(生菜我都不想点)......在我们的程序中不可能穷举出所有的组合而事先实现所有的种类,我们只有让顾客在选择菜品时不断地动态地生成新的种类来满足顾客的需求。
动态生成新的种类,可以在之前已有的菜品的基础上增加生成新的菜品。也就是往已有的菜品中加菜,进行装饰或者包裹。如下图所示,按照用户点菜的顺序依次包裹形成最终的菜品。 这种采用层层包裹层层点缀生成新对象的方式就叫做装饰者模式。
实现
使用装饰者模式的首要要求就是装饰者和被装饰者必须具有相同的超类型。如下图所示,生菜、豆腐等装饰者都继承自HotPot类,这种继承结构总是让人感觉不舒服,因为毕竟火锅中加的菜一般我们不称之为火锅。但是,这种模式状态下请忽略这种字面的纠结,我们使用继承只是为了有正确的类型而不是继承它的行为。因为有了正确的类型,我们可以在对被装饰者进行装饰时,可以调用相同的api并对装饰者的进行行为的扩展。
public abstract class HotSpot {
String description = "不知道什么火锅";
public String getDescription() {
return description;
}
public abstract double cost();
}
public class TwoFlavorHotPot extends HotSpot {
public TwoFlavorHotPot() {
description = "鸳鸯锅";
}
@Override
public double cost() {
return 48;
}
}
public class SpicyHotPot extends HotSpot{
public SpicyHotPot() {
description = "香辣锅";
}
@Override
public double cost() {
return 58;
}
}
public class MushroomHotSpot extends HotSpot{
public MushroomHotSpot() {
description = "菌菇锅";
}
@Override
public double cost() {
return 38;
}
}
public abstract class CondimentDecorator extends HotSpot{
public abstract String getDescription();
}
public class Lettuce extends CondimentDecorator {
HotSpot hotSpot ;
public Lettuce(HotSpot hotSpot) {
this.hotSpot = hotSpot;
}
@Override
public String getDescription() {
return hotSpot.getDescription() + "加一份生菜";
}
@Override
public double cost() {
return hotSpot.cost() + 10;
}
}
public class Tofu extends CondimentDecorator {
HotSpot hotSpot;
public Tofu(HotSpot hotSpot) {
this.hotSpot = hotSpot;
}
@Override
public String getDescription() {
return hotSpot.getDescription() + "加一份豆腐";
}
@Override
public double cost() {
return hotSpot.cost() + 10;
}
}
public class Mutton extends CondimentDecorator {
HotSpot hotSpot ;
public Mutton(HotSpot hotSpot) {
this.hotSpot = hotSpot;
}
@Override
public String getDescription() {
return hotSpot.getDescription() + "加一份羊肉";
}
@Override
public double cost() {
return hotSpot.cost() + 58;
}
}
public class Beaf extends CondimentDecorator {
HotSpot hotSpot ;
public Beaf(HotSpot hotSpot) {
this.hotSpot = hotSpot;
}
@Override
public String getDescription() {
return hotSpot.getDescription() + "加一份牛肉";
}
@Override
public double cost() {
return hotSpot.cost() + 48;
}
}
测试
public class Test {
public static void main(String[]args){
//单锅底
HotSpot hotSpot = new MushroomHotSpot();
System.out.println(hotSpot.getDescription() + "。价格:" + hotSpot.cost()+"。");
//一份生菜的鸳鸯锅
HotSpot hotSpot1 = new TwoFlavorHotPot();
hotSpot1 = new Lettuce(hotSpot1);
System.out.println(hotSpot1.getDescription() + "。价格:" + hotSpot1.cost()+"。");
//样样都有的香辣锅
HotSpot hotSpot2 = new SpicyHotPot();
hotSpot2 = new Lettuce(hotSpot2);
hotSpot2 = new Tofu(hotSpot2);
hotSpot2 = new Mutton(hotSpot2);
hotSpot2 = new Beaf(hotSpot2);
System.out.println(hotSpot2.getDescription() + "。价格:" + hotSpot2.cost()+"。");
}
}
菌菇锅。价格:38.0。
鸳鸯锅加一份生菜。价格:58.0。
香辣锅加一份生菜加一份豆腐加一份羊肉加一份牛肉。价格:184.0。
从上述的测试代码中我们可以看出,我们可以对既有的底锅进行任意菜品的添加,可以在不用修改原有代码的基础上动态生成顾客所需的菜,满足顾客的需求。
总结
优点
- 装饰者和被装饰者对象具有相同的超类型。继承是为了有正确的类型,而不是继承它的行为。利用继承达到“类型匹配”,而不是利用继承获得“行为”。
- 可以用一个或者多个装饰者包裹一个对象。
- 既然装饰者和被装饰者具有相同的超类型,所以在任何需要原始对象(包裹对象)的场合,可以使用装饰过的对象代替它。
- 装饰者可以在被装饰者的行为之前或者之后加上自己的行为,已达到特定的目标,例如此例中的生成描述信息和最终价格。
- 对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象。
缺点
- 生成很多很多的小类,代码层面比较啰嗦。如例所示,我们要创建各种装饰对象并且不停地进行new操作来完成包裹。
- 在装饰的过程中,由于装饰者没有窥视被装饰的对象,所以在此过程中,无法针对特定的类型进行特定操作,例如此例中对鸳鸯锅进行8折的优惠操作,那么在不断地装饰过程中,后面的装饰者是不好进行被装饰者类型的判读的。
- 进行行为的扩展只能一次装饰一次扩展,装饰者没有判断其他装饰者的存在的能力,例如此例中加2份羊肉的话,最终生成的描述信息是“...加一份羊肉加一份羊肉...”而不是“...加2份羊肉...”,要达到这种效果就需要额外增加代码了。
参考
- HeadFirst 设计模式(中文版)