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