文章大纲

细致入微地讲解php设计模式:装饰器模式

2020-03-31 22:06:31

说到装饰,就是像在礼品外面包一层礼物盒一样。但装饰器模式,除了装饰以外,它主要的目的是想在礼品的基础上进行功能拓展,而又不对礼品本身造成改动。


下面是装饰器模式的类图:



按照上面类图的思想,我写了一个手抓饼的示例程序,下面是主体调用的代码:

spl_autoload_register(function ($class) {
    include strtolower($class) . '.php';
});

$pancake = new Pancake();
$decorate = new AddEgg($pancake);
$decorate = new AddHam($decorate);
$decorate = new AddCheese($decorate);
echo "the price is:".$decorate->getPrice();
$decorate->madeStep();

整个程序,可以去翟码农github里下载:php设计模式代码示例


不知道手抓饼具体英文,就用煎饼这个单词Pancake替代了。


Pancake就相当于礼品,而加鸡蛋、加火腿、加芝士的这些AddEgg、AddHam、AddCheese类,则相当于装饰类——礼品的包装盒。


装饰器模式有什么好处?

Pancake类里有getPrice和madeStep方法,代码如下:

include_once "Decorator.php";
class Pancake implements IDecorator
{
    /**
     * 获取价格
     * @return mixed
     */
    public function getPrice(){
        //单饼5元
        return 5;
    }

    /**
     * 烹饪步骤
     * @return mixed
     */
    public function madeStep(){
        echo "<br>fetch one pancake and bake it<br>";
    }

}


顾客要手抓饼,有的要加鸡蛋,有的要加火腿,有的要全都加,价钱都不一样。如果频繁去改Pancake的getPrice方法,或者是在Pancake类里加一大堆类似getPriceWithEgg,getPriceWithHam的方法,就违反了类的开闭原则:对扩展开放,对修改关闭。


如果将AddEgg、AddHam等设计成装饰类,顾客要什么样子的煎饼,在上面主体代码里将装饰后的对象传进装饰类即可,就无须去修改Pancake本身的getPrice方法了。


而且哪天可以加鸡排了,就直接新加一个鸡排的装饰类,也是不需要对之前的其它类里面的代码进行改动。这就是所谓的“易扩展”。


装饰器模式有什么坏处?

所谓坏处,指的也就是装饰器模式在哪些场景不适合用。


继续拿手抓饼的例子来说,如果顾客要加2个鸡蛋,或是3个鸡蛋,抑或是4个鸡蛋,光鸡蛋这块,在主体代码里就会展示如下:

$pancake = new Pancake();
$decorate = new AddEgg($pancake);
$decorate = new AddEgg($pancake);
$decorate = new AddEgg($pancake);


鸡蛋、火腿等要得越多,上面代码就会越冗余。


同样,当手抓饼配料非常多时,即不止本文提到的鸡蛋、火腿、芝士这3类,可能还有鸡柳、生菜、火腿肠等等,也不太适合使用装饰器模式,因为这样设计,会造成装饰者类过于繁多。


总结起来就是如下:

装饰者类型繁多,或是单一类型的实例数量又不少于1时,这些场景不适合用装饰器模式


装饰器设计模式的优缺点谈完了,那接下来就针对上面装饰器的设计模式类图,来具体分析为何要那样设计。


一开始看类图时,我很纳闷,为何被装饰者——类图中的ConcreteComponent类——需要和装饰者一同实现Component接口呢?


像下面这样设计,不香么?

class Pancake
{
    public function getPrice(){
        return 5;
    }
}

class AddEgg
{
    private $obj;
    public function __construct(Pancake $obj)
    {
        $this->obj = $obj;
    }

    public function getPrice(){
        return $this->obj->getPrice() + 1;
    }
}

$pancake = new Pancake();
$decorate = new AddEgg($pancake);
echo $decorate->getPrice();


说到这,就涉及到设计模式SOLID原则中的一个原则:让类依赖抽象,而不是细节。


上面代码里,AddEgg构造方法,传入的是Pancake类,这就属于依赖于细节了。


依赖细节带来的问题就是耦合性太高了,如果店主不再卖手抓饼,而改换成卖馕了,就需要将AddEgg里的Pancake类换成馕类了,这样就又违反了前面所提到的“类的开闭原则”。


对上面代码稍稍加工,就变成如下:

interface IComponent
{
    public function getPrice();
}

class Pancake implements IComponent
{
    public function getPrice(){
        return 5;
    }
}

class AddEgg
{
    private $obj;
    
    //依赖于抽象,Pancake具体类换成Icomponent接口了
    public function __construct(IComponent $obj)
    {
        $this->obj = $obj;
    }

    public function getPrice(){
        return $this->obj->getPrice() + 1;
    }
}

$pancake = new Pancake();
$decorate = new AddEgg($pancake);
echo $decorate->getPrice();


这样子加工后,以后Pancake换成任何一种薄饼,AddEgg类(装饰者)就可以不做任何改动。


跟上面装饰器设计模式的类图对比,就会发现几乎一致了。


但还有一个问题:为何让装饰者和被装饰者实现同一个接口呢?


我给装饰者准备一个接口,被装饰者再单独准备一个接口,不就可以了么?


接口是一种契约,通过示例代码可以看出,我要计算加了配菜后的薄饼价格,始终用的getPrice方法。如果没有同一接口做约束,可能Pancake实现的接口里是getPrice方法,而你在装饰者的接口里用的可能是getTotalPrice,这样子就给主体里的代码带来了复杂度的提高。


例如没加配菜时,主体代码是这样:

$pancake = new Pancake();
echo $pancake->getPrice();


加了配菜,主体代码如下:

$pancake = new Pancake();
$pancake = new AddEgg($pancake);
//这里要调用getTotalPrice方法
echo $pancake->getTotalPrice();


一会儿用getPrice方法,一会儿用getTotalPrice方法,能简单的问题就尽量简单化,直接无脑用getPrice方法不香么?


类图中设计,还引入了一个Decorator的抽象类,好处在哪里?

看上面AddEgg类的构造方法,你可能就已经体会出来了。这里就简单点拨下,推荐大家自己也亲自动手敲敲实践一下:


引入Decorator抽象类的好处:

被装饰者(按类图翻译则叫组件)注入到装饰者里,这个操作可以放到Decorator抽象类中,这样子就无需在每个装饰者(AddEgg、AddHam等类)里写这个构造函数了,引用组件对象的私有属性也是如此,不过为了能让属性被子类继承,属性方法Decorator抽象类里,就要用protected关键字了。


说了半天,装饰者为啥要注入组件呢?

从手抓饼的实例程序看,还是很容易理解的。不注入手抓饼Pancake类的话,在AddEgg类里我就无法知道单饼的价格,这样子我就无法获得“加了鸡蛋的手抓饼的价格”。


从装饰者的字面意义来理解,装饰本就是包在礼品外面。注入行为,就正像把礼品装进礼品包装盒一样。


ok,到这里,装饰器模式的整个类图思想也就梳理完了。


最后对装饰器模式类图设计的主要点再进行归纳总结下:

1、组件和装饰类,都实现同一接口。
2、装饰类通过接口注入组件。
3、在接口和具体装饰类之间,引入抽象装饰类


本文为翟码农个人博客下有关设计模式的原创文章,转载请注明出处:http://www.zhai14.com/blog/design-pattern-of-decorator-analyzed-with-simple-thought.html




我要评论
评论列表