定义
动态地将责任附加到对象上。想要扩展功能,装饰者提供有别于继承的另一种选择。
例子
现在有一家咖啡店,需要设计一个咖啡的订单系统。在最初咖啡种类较少的时候,设计了一个beverage的父类,所有的咖啡都是继承自beverage的子类:
这个设计采用继承的方式实现不同咖啡子类,现在需求有了变化,在购买咖啡的时候,可以在里面加入各种调料,比如牛奶,豆浆,摩卡等,这些调料也是有各自的价格。如何处理这个需求,并应对可能的变化?
分析
我们可能有以下思路:
1.在现有设计下,增加新的子类。如豆浆浓缩咖啡,牛奶浓缩咖啡,豆浆深培咖啡等。
分析:这种方式本质是穷举法,只是理论可行而已。看起来是对咖啡 + 调料的排列组合,一一列举出来作为一个子类。事实上,用户完全可能需要不止一种的调料,保持这个设计将是灾难一样。
2.上面思路不可行的原因在于,我们不知道可能会有多少组合以及按照什么样的顺序去组合。这里我们需要想明白一件事,即加了调料的饮料依然是饮料,继续加调料还是饮料。这个事实给我们的启发就是,我们可以设计出一个组合的对象,它组合了饮料对象和调料对象。使用起来大概是这样的:
Beverage A = new Beverage(some beverage, milk);
A = new Beverage(A, soy);
A = new Beverage(A, suger);
...
3.上述组合对象的思想是没问题的,只是实现起来有些问题。在上面的伪代码可以看出, 设计的饮料对象需要知道所有的饮料类(咖啡,奶茶等)和所有的调料类(豆浆,牛奶,摩卡等)。这个问题可以用继承或接口实现,组合对象里包含两个对象,分别继承于(或实现接口)饮料类和调料类
4.继续优化,从实际意义看,调料类是依赖于饮料的,即饮料是调料的必要不充分条件,所以没必要设计一系列的调料对象,调料只是装饰饮料用的。每一种调料是一种装饰者,需要饮料来初始化,而这个装饰器继承自饮料类(或者实现饮料接口)。类图如下:
装饰者模式的关键在于,装饰者和被装饰者有着相同的超类型,这里用到继承或者接口的手段目的都是为了做类型匹配,另外,装饰者可以再所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的,典型的例子就是java中的io类,装饰者模式利用了组合对象来实现扩展功能。
总结
- 继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式
- 在我们的设计中,应该允许行为可以被扩展,而无须修改现有的代码
- 组合和委托可用于在运行时动态地加上新行为
- 除了继承,装饰者模式也可以让我们扩展行为
- 装饰者模式意味着一群装饰者类,这些类用来包装具体组件
- 装饰者类反映出被装饰的组件类型(具有相同的类型)
- 可以使用无数个装饰者包装一个组件
- 装饰者会导致设计中出现很多小对象,过度使用,会让程序变得复杂