设计模式:灵活编程(装饰模式)

装饰模式,英文叫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 出现。感谢阅读,再会!

posted @ 2018-06-20 11:01  BNDong  阅读(701)  评论(0编辑  收藏  举报