通俗易懂的谈谈装饰器模式
29

前言

在编码的时候,我们为了扩展一个类经常是使用继承方式来实现,随着扩展功能的增多,子类会越来越膨胀,使系统变得不灵活。

装饰器模式( Decorator Pattern )允许向一个现有的对象添加新的功能,同时又不改变其结构。它能让我们在扩展类的时候让系统较好的保持灵活性。

那么装饰器模式具体是什么样的呢?

从一个情景开始

我们有一块地,在这块地上,我们要盖一栋有好几间房间的别墅,每间房间的装修费用都不同,现在,我们要对盖别墅的费用进行计算。

先定义一个 Land 类,表示这块地,Land 类定义了在这块地上盖别墅需要花钱这个规则。

abstract class Land
{
    abstract public function cost();
}

Land 已经定义好了在这块地上盖房需要花钱的这个规则了,但是盖一间房间具体花多少钱呢?

此时我们再定义一个 Room 类,这个类具体的定义了一个房间建造的基本费用(一个最简单房间,里面啥也没有的)。

class Room extends Land
{
    private $money = 1000;
    public function cost()
    {
        return $this->money;
    }
}

然后开始建造房间,我们建了两个房间,分别是客厅和餐厅,用 LivingRoom 类和 DiningRoom 类来表示

class LivingRoom extends Room
{
    public function cost()
    {
        return parent::cost()+200; //客厅的建造费用在房屋建造费用的基础上多200,比如要买沙发,电视
    }

}

class DiningRoom extends Room
{
    public function cost()
    {
        return parent::cost()+100; //餐厅的建造费用在房屋建造费用的基础上多100,比如买餐桌
    }
}

现在,我们很容易就能得到建造一间客厅所需的花费

$livingRoomCost = new LivingRoom();
echo $livingRoomCost->cost();

问题的产生

不过,这样的结构并不具备灵活性,虽然我们可以很容易的分别得出建造一间客厅和建造一间餐厅的费用,但是,如果我买的地比较小,只能把餐厅和客厅建在同一个房间里,那要怎么去计算费用?难道还要很麻烦的去创建一个包含客厅和餐厅的 LivingDiningRoom 类?这样做的话除了麻烦,还会使代码产生重复。

解决问题

为了更好的解决这个问题,我们得做一些调整,同样先声明 Land 类和 Room 类,不同的是,引入了一个房间的装饰类 RoomDecorator,它继承了 Land 类,因为没有实现 Land 类的 cost() 方法,所以需将它声明为抽象类,并且定义了一个以 Land 类的对象为参数的构造方法,传入的对象会保存在 $land 属性中,该属性声明为 protected ,以便子类访问。具体如下。

abstract class RoomDecorator extends Land
{
    protected $land;
    public function __construct(Land $land)
    {
        $this->land = $land;
    }
}

然后我们再重新定义客厅类和餐厅类

class LivingRoom extends RoomDecorator
{
    public function cost()
    {
        return $this->land->cost()+200;
    }
}

class DiningRoom extends RoomDecorator
{
    public function cost()
    {
        return $this->land->cost()+100;
    }
}

这两个类都扩展自 RoomDecorator 类,这意味着它们都拥有指向 Land 对象的引用。当它们的 cost() 方法被调用时,都会先调用所引用的 Land 类对象的 cost() 方法,然后再执行自己特有的操作。

所以这时候,建造一间客厅所需的费用是这样计算

$livingRoomCost = new LivingRoom(new Room());
echo $livingRoomCost->cost(); //输出1200

建造一间餐厅所需的费用是这样计算

$diningRoomCost = new DiningRoom(new Room());
echo $diningRoomCost->cost(); //输出1100

回到刚才的问题,如果我们需计算建造一间包含客厅餐厅的房间所需费用,代码如下

$livingRoom = new DiningRoom(new LivingRoom(new Room()));
echo $livingRoom->cost(); //输出1300

看,我们现在计算建造费用的思路是:计算出基础房间的费用 --> 在基础房间上装饰成客厅的费用 --> 在客厅的基础上加装饰餐厅的费用 --> 得到包含客厅餐厅的房间费用。已经不需要麻烦的通过创建一个 LivingDiningRoom 类来计算包含客厅餐厅的房间建造费用了。

这便是装饰模式,通过一层一层的装饰,我们可以灵活的得到我们想要的结果。可以轻松的添加新的装饰器类或者新的组件来创建灵活的结构。

完整代码

<?php

abstract class Land
{
    abstract function cost();
}

class Room extends Land
{
    private $money = 1000;
    public function cost()
    {
        return $this->money;
    }
}

//装饰器
abstract class RoomDecorator extends Land
{
    protected $land;
    public function __construct(Land $land)
    {
        $this->land = $land;
    }
}

class LivingRoom extends RoomDecorator
{
    public function cost()
    {
        return $this->land->cost()+200;
    }
}

class DiningRoom extends RoomDecorator
{
    public function cost()
    {
        return $this->land->cost()+100;
    }
}

$livingRoomCost = new LivingRoom(new Room());
echo $livingRoomCost->cost(); //输出1200

$diningRoomCost = new DiningRoom(new Room());
echo $diningRoomCost->cost(); //输出1100

$livingDining = new DiningRoom(new LivingRoom(new Room()));
echo $livingDining->cost(); //输出1300

the end.

happy coding! ^_^

临渊羡鱼,退而结网。

本帖由 Summer 于 1年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 8

希望可以多出一些设计模式的文章:thumbsup:

1年前

描述好接地气,赞一个 :+1:

1年前
Jeffrey_up

@to2False :smiley:

1年前
米休love

为什么我感觉变的更复杂了呢,,,

1年前

装饰模式就是重写父类一个方法,在这个方法中,对父类的某个属性进行再加工修改。这是最基本的用法,还有更复杂的应用,比如过滤http 请求,具体参考laravel 的middleware ,middleware 就是应用的装饰模式。

1年前
keer

新手看着感觉好复杂。 根本不知道在项目里要如何用,该在什么地方用。尴尬

1年前
Jeffrey_up

@keer 楼上的回答有说了,在框架的源码中会用到这种设计模式,可以看看~

1年前
wanghan

这个也要赞一下!希望可以连载!

2个月前

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!