《Head First 设计模式》中描述了一个场景:星巴兹(Starbuzz)咖啡店的咖啡种类(HouseBlend、DarkRoast、Decaf、Espresso 等)增长迅速,同时客户购买咖啡时,要求在其中加入各种调料,例如:蒸奶(Milk)、豆浆(Soy)、摩卡(Mocha)或覆盖奶泡(Whip),订单系统需要根据咖啡种类和所加入的调料收取不同的费用,需要设计一个支持这些功能的订单系统。
糟糕的设计
在支持调料之前,星巴兹订单系统的设计如上图所示,为支持新功能,最简单粗暴的方式就是:为每种咖啡和调料的组合生成一个新类,例如:HouseBlendWithMilkAndMocha,每个类覆盖 cost() 方法返回对应的价格就可以了。但是,随着咖啡和调料种类的增多,必然会造成类的极具膨胀,而且每新增一种咖啡或调料,都要对现有代码逻辑做很多侵入性的修改,最终导致无法维护。
另外一种可行的方案是:在 Berverage 类中设置各种调料是否添加的布尔值,其 cost() 方法计算要加入的各种调料的价格;子类仍将覆盖 cost() 方法,但是会调用父类的 cost(),计算出咖啡加上调料的价格:
public class Beverage { |
但是这个方案仍然有很多问题:
- 调料价格的改变会使我们更改现有代码
- 一旦出现新的调料,需要在超类添加新的布尔值,并改变超类中的 cost() 方法
- 以后可能会开发出新咖啡,某些调料可能并不适合加入,但是这个设计中每种咖啡都继承了所有调料的方法
- 万一顾客想要双倍摩卡咖啡怎么办
装饰者模式
到目前为止,星巴兹订单系统遇到的问题有:类数量爆炸、设计死板,以及基类中加入的新功能并不适用于所有的子类。我们需要转换一下思路:以咖啡为主体,然后用调料来「装饰」咖啡,例如顾客想要摩卡深焙咖啡,要做的是:
- 拿一个深焙咖啡(DarkRoast)对象
- 以摩卡(Mocha)对象装饰它
- 调用 cost() 方法,并依赖委托(delegate)将调料的价钱加上去
public abstract class Beverage { |
IO 库中的样例
Java IO 库也是用了装饰者模式,InputStream 是抽象基类,FileInputStream 等是具体的实现类,FilterInputStream 是一个抽象装饰类,BufferedInputStream 等是具体的装饰者,上面的类图做了简化,只展示了读取字符的方法 read(),我们也可以实现一个自己的装饰者类,只需要继承 FilterInputStream 即可:
public class LowerCaseInputStream extends FilterInputStream { |
从上面两个使用装饰者模式的场景可以看出:
- 装饰者和被装饰对象有相同的超类型
- 可以用一个或多个装饰者包装一个对象
- 装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,已达到特定的目的