前言

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

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

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

从一个情景开始

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

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

1
2
3
4
abstract class Land
{
abstract function cost();
}

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

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

1
2
3
4
5
6
7
8
class Room extends Land
{
private $money = 1000;
public function cost()
{
return $this->money;
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class LivingRoom extends Room
{
public function cost()
{
return parent::cost()+200; //客厅的建造费用在房屋建造费用的基础上多200,比如要买沙发,电视
}
}
class DiningRoom extends Room
{
public function cost()
{
return parent::cost()+100; //餐厅的建造费用在房屋建造费用的基础上多100,比如买餐桌
}
}

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

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

问题的产生

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

解决问题

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

1
2
3
4
5
6
7
8
abstract class RoomDecorator extends Land
{
protected $land;
public function __construct(Land $land)
{
$this->land = $land;
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()方法,然后再执行自己特有的操作。

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

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

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

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

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

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

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

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

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?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! ^_^