装饰模式,英文叫 Decorator Pattern,又叫装饰者模式。装饰模式是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
在设计模式中,一直推荐多用组合,少用继承的结构。使用组合来处理对象的行为,可以在运行时动态的进行扩展。而继承是共享父类特性的一种简单的方法,但可能会使你将需要改变的特性硬编码到继承体系中,而这常会降低系统灵活性。
问题
将所有功能都寄托于继承体系上会导致类的增多,代码的冗余!
首先我定义一个饮品类 Drink
及其子类 Cappuccino
:
1 | abstract class Drink |
getPrice
方法用于返回当前饮品的价格,对于单一价格来说,这种结构还不错。但是,如果我希望 Cappuccino
加糖和加牛奶都会加收金额,并且获取到加价后的价格怎么处理?有一个办法是从 Cappuccino
对象派生:
1 | class CappuccinoAddSugar extends Cappuccino |
这样就可以很轻松获得一个加糖的卡布奇诺价格:
1 | $price = new CappuccinoAddSugar(); |
看到这也能看到这种结构的弊端,如果我们需要既加糖又加奶的价格呢?OK,可以再加一个派生类。但是,如果需要加蜂蜜的呢?既加蜂蜜又加牛奶的呢?…… 随着功能增多你会发现类成“爆炸式”的增长,而且代码结构都是重复的。
实现
装饰模式使用组合和委托而不是只使用继承来解决功能变化的问题。装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。通过使用不同的装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。
我重写下上面的示例:
1 | abstract class Drink |
这里我引入了一个新类 DrinkDecorator
,定义了以一个 Drink
对象为参数的构造方法,传入的对象被保存在 $drink
属性中,该属性被声明为 protected
,以便子类可以访问它。下面我重新定义 CappuccinoAddSugar
和 CappuccinoAddMilk
类。
1 | class CappuccinoAddSugarDecorator extends DrinkDecorator |
这些类都扩展自 DrinkDecorator
类,这意味着它们拥有指向 Drink
对象的引用。当 getPrice
方法被调用时,这些类都会先调用所引用的 Drink
对象的 getPrice
方法,然后执行自己特有的操作。
通过像这样使用组合和委托,可以在运行时轻松地合并对象。
1 | // 基础价格 |
这样的模型极具扩展性。可以非常轻松地添加新的装饰器或者新的组件。通过使用大量的装饰器,可以在运行时创建极为灵活的结构。
总结
组合和继承通常都是同时使用的。装饰模式提供了更多的灵活性,但也提高了代码的复杂性。因为装饰对象作为子对象的包装,所以保持基类中的方法尽可能少是很重要的。如果一个基类具有大量特性,那么装饰对象不得不为它们包装的对象的所有 public 方法加上委托。你可以用一个抽象的装饰类来实现,不过这仍旧会带来耦合,并可能导致 bug 出现。感谢阅读,再会!