PHP设计模式之装饰器模式

组合模式帮助我们聚合组件,而装饰模式则使用类似结构来帮助我们改变具体组件的功能。该模式同样体现了组合的重要性,但组合是在代码运行时实现的。
装饰模式是在不必改变原类文件和使用继承的情况下,动态的扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

实例一:魔兽争霸的不同场景

魔兽争霸区域经验获取第一版:平原、干净的平原、污染的平原

demo1.php

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
<?php
header('Content-Type:text/html; charset=utf-8');

// 区域类
abstract class Tile{
abstract public function exprience();
}

// 平原类
class Plain extends Tile{
private $exprience = 2;
public function exprience(){
return $this->exprience;
}
}

// 干净的平原
class CleanPlain extends Plain{
public function exprience(){
return parent::exprience() + 2;
}
}

// 污染的平原
class PollutedPlain extends Plain{
public function exprience(){
return parent::exprience() - 4;
}
}


$plain = new Plain();
echo '在普通平原的经验值为'.$plain->exprience();

echo '<br />';

$clean_plain = new CleanPlain();
echo '在干净平原的经验值为'.$clean_plain->exprience();

echo '<br />';

$polluted_plain = new PollutedPlain();
echo '在污染平原的经验值为'.$polluted_plain->exprience();

此版本使用了继承,设定了一个总的抽象区域类,然后定义了一个平原区域继承区域类 。平原区域设定了正常的经验获取值的方法 exprience()。平原区域还细分成干净的平原和污染的平原,他们在平原区域的经验获取值的基础上增加和减少经验。
现在要扩展这个系统,比如:增加第三个平原区,既干净又污染,怎么处理呢?或者 :增加第二个区域,高原区,里面也分为干净的高原区,污染的高原区,既干净又污染的高原区。

魔兽争霸区域经验获取第二版:增加既干净又污染的区域

demo2.php

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
53
54
55
56
<?php
header('Content-Type:text/html; charset=utf-8');

// 区域类
abstract class Tile{
abstract public function exprience();
}

// 平原类
class Plain extends Tile{
private $exprience = 2;
public function exprience(){
return $this->exprience;
}
}

// 干净的平原
class CleanPlain extends Plain{
public function exprience(){
return parent::exprience() + 2;
}
}

// 污染的平原
class PollutedPlain extends Plain{
public function exprience(){
return parent::exprience() - 4;
}
}

// 既干净又污染的平原
class CleanPollutedPlain extends Plain{
public function exprience(){
$clean_plan = new CleanPlain();
$polluted_plain = new PollutedPlain();
return $clean_plan->exprience() + $polluted_plain->exprience() - parent::exprience();
}
}

$plain = new Plain();
echo '在普通平原的经验值为'.$plain->exprience();

echo '<br />';

$clean_plain = new CleanPlain();
echo '在干净平原的经验值为'.$clean_plain->exprience();

echo '<br />';

$polluted_plain = new PollutedPlain();
echo '在污染平原的经验值为'.$polluted_plain->exprience();

echo '<br />';

$clean_polluted_plain = new CleanPollutedPlain();
echo '在既干净又污染平原的经验值为'.$clean_polluted_plain->exprience();

此版本增加了既干净又污染的区域,通过合并干净和污染的经验值,得到既干净又污染的区域的经验值。

魔兽争霸区域经验获取第三版:增加高原区

demo3.php

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?php
header('Content-Type:text/html; charset=utf-8');

// 区域类
abstract class Tile{
abstract public function exprience();
}

// 平原类
class Plain extends Tile{
private $exprience = 2;
public function exprience(){
return $this->exprience;
}
}

// 干净的平原
class CleanPlain extends Plain{
public function exprience(){
return parent::exprience() + 2;
}
}

// 污染的平原
class PollutedPlain extends Plain{
public function exprience(){
return parent::exprience() - 4;
}
}

// 既干净又污染的平原
class CleanPollutedPlain extends Plain{
public function exprience(){
$clean_plan = new CleanPlain();
$polluted_plain = new PollutedPlain();
return $clean_plan->exprience() + $polluted_plain->exprience() - parent::exprience();
}
}

// 高原类
class Highland extends Tile{
private $exprience = 3;
public function exprience(){
return $this->exprience;
}
}

// 干净的高原
class CleanHighland extends Highland{
public function exprience(){
return parent::exprience() + 2;
}
}

// 污染的高原
class PollutedHighland extends Highland{
public function exprience(){
return parent::exprience() - 4;
}
}

// 既干净又污染的高原
class CleanPollutedHighland extends Highland{
public function exprience(){
$clean_highland = new CleanHighland();
$polluted_highland = new PollutedHighland();
return $clean_highland->exprience() + $polluted_highland->exprience() - parent::exprience();
}
}


$plain = new Plain();
echo '在普通平原的经验值为'.$plain->exprience();

echo '<br />';

$clean_plain = new CleanPlain();
echo '在干净平原的经验值为'.$clean_plain->exprience();

echo '<br />';

$polluted_plain = new PollutedPlain();
echo '在污染平原的经验值为'.$polluted_plain->exprience();

echo '<br />';

$clean_polluted_plain = new CleanPollutedPlain();
echo '在既干净又污染平原的经验值为'.$clean_polluted_plain->exprience();

echo '<br /><br />';

$highland = new Highland();
echo '在普通高原的经验值为'.$highland->exprience();

echo '<br />';

$clean_highland = new CleanHighland();
echo '在干净高原的经验值为'.$clean_highland->exprience();

echo '<br />';

$polluted_highland = new PollutedHighland();
echo '在污染高原的经验值为'.$polluted_highland->exprience();

echo '<br />';

$clean_polluted_highland = new CleanPollutedHighland();
echo '在既干净又污染高原的经验值为'.$clean_polluted_highland->exprience();

此版本增加了高原区,分别还有干净的,污染的,既干净又污染的。用继承树来看这些类,你会发现有大量的重复,大量冗余的类。这个时候,我们就需要考虑装饰器模式来改写这种继承模式。

魔兽争霸区域经验获取第四版:使用装饰器来简化代码,提高扩展维护灵活性

demo4.php

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php
header('Content-Type:text/html; charset=utf-8');

// 区域类
abstract class Tile{
abstract public function exprience();
}

// 平原类
class Plain extends Tile{
private $exprience = 2;
public function exprience(){
return $this->exprience;
}
}

// 高原类
class Highland extends Tile{
private $exprience = 3;
public function exprience(){
return $this->exprience;
}
}

// 装饰器类
abstract class Decorator extends Tile{
protected $tile;
public function __construct(Tile $tile){
$this->tile = $tile;
}
}

// 干净的
class Clean extends Decorator{
public function exprience(){
return $this->tile->exprience() + 2;
}
}

// 污染的
class Polluted extends Decorator{
public function exprience(){
return $this->tile->exprience() - 4;
}
}

$plain = new Plain();
echo '在普通平原的经验值为'.$plain->exprience();

echo '<br />';

$clean_plain = new Clean(new Plain());
echo '在干净平原的经验值为'.$clean_plain->exprience();

echo '<br />';

$polluted_plain = new Polluted(new Plain());
echo '在污染平原的经验值为'.$polluted_plain->exprience();

echo '<br />';

$clean_polluted_plain = new Clean(new Polluted(new Plain()));
echo '在既干净又污染平原的经验值为'.$clean_polluted_plain->exprience();

echo '<br /><br />';

$highland = new Highland();
echo '在普通高原的经验值为'.$highland->exprience();

echo '<br />';

$clean_highland = new Clean(new Highland());
echo '在干净高原的经验值为'.$clean_highland->exprience();

echo '<br />';

$polluted_highland = new Polluted(new Highland());
echo '在污染高原的经验值为'.$polluted_highland->exprience();

echo '<br />';

$clean_polluted_highland = new Clean(new Polluted(new Highland()));
echo '在既干净又污染高原的经验值为'.$clean_polluted_highland->exprience();

此版本和第三版,多了一个装饰器类,而少了大量的冗余类。而且使用装饰器,可以避免比如既干净又污染的组合类的产生。在扩展维护上大大提高了灵活性,新增一个区域(平原和高原的区域),或者新增一个类型区域(干净或污染的经验值区域),都不需要修改别的类。

装饰模式的特点:

  1. 装饰对象和真实对象有相同的接口。这样客户端对象就可以以和真实对象相同的方式和装饰对象交互。
  2. 装饰对象包含一个真实对象的索引(reference)
  3. 装饰对象接受所有的来自客户端的请求。它把这些请求转发给真实的对象。
  4. 装饰对象可以在转发这些请求以前或以后增加一些附加功能。这样就确保了在运行时,不用修改给定对象的结构就可以在外部增加附加的功能。在面向对象的设计中,通常是通过继承来实现对给定类的功能扩展。

实例二:设置画布颜色

demo5.php

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
<?php
header('Content-Type:text/html; charset=utf-8');

class Canvas{
private $data;

function __construct($width = 20, $height = 10){
$data = [];
for($i = 0;$i < $height;$i++){
for($j = 0;$j < $width;$j++){
$data[$i][$j] = '*';
}
}
$this->data = $data;
}

function draw(){
foreach($this->data as $line){
foreach($line as $char){
echo $char;
}
echo "<br />\n";
}
}
}

$canvas = new Canvas();
$canvas->draw();

设置颜色

demo6.php

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
<?php
header('Content-Type:text/html; charset=utf-8');

class Canvas{
private $data;

function __construct($width = 20, $height = 10){
$data = [];
for($i = 0;$i < $height;$i++){
for($j = 0;$j < $width;$j++){
$data[$i][$j] = '*';
}
}
$this->data = $data;
}

function draw(){
foreach($this->data as $line){
foreach($line as $char){
echo $char;
}
echo "<br />\n";
}
}
}

class Canvas2 extends Canvas{
private $color;

public function __construct($color){
$this->color = $color;
}

public function draw(){
echo '<div style="color:'.$this->color.';">';
parent::draw();
echo "</div>";
}
}

$canvas = new Canvas2('green');
$canvas->draw();

使用装饰器模式设置颜色

demo7.php

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
header('Content-Type:text/html; charset=utf-8');

interface DrawDecorator{
public function beforeDraw();
public function afterDraw();
}

class Canvas{
private $data;
private $decorators = [];

function __construct($width = 20, $height = 10){
$data = [];
for($i = 0;$i < $height;$i++){
for($j = 0;$j < $width;$j++){
$data[$i][$j] = '*';
}
}
$this->data = $data;
}

function draw(){
$this->beforeDraw();
foreach($this->data as $line){
foreach($line as $char){
echo $char;
}
echo "<br />\n";
}
$this->afterDraw();
}

function addDecorator(DrawDecorator $decorator){
$this->decorators[] = $decorator;
}

public function beforeDraw(){
foreach($this->decorators as $decorator){
$decorator->beforeDraw();
}
}

public function afterDraw(){
$decorators = array_reverse($this->decorators);
foreach($decorators as $decorator){
$decorator->afterDraw();
}
}
}

class ColorDrawDecorator implements DrawDecorator{
private $color;

public function __construct($color){
$this->color = $color;
}

public function beforeDraw(){
echo '<div style="color:'.$this->color.';">';
}

public function afterDraw(){
echo '</div>';
}
}

$canvas = new Canvas();
$canvas->addDecorator(new ColorDrawDecorator('green'));
$canvas->draw();

  1. 装饰器模式(Decorator),可以动态地添加修改类的功能。
  2. 一个类提供了一项功能,如果要在修改并添加额外的功能,传统的编程模式,需要编写一个子类继承它,并重新实现类的方法。
  3. 使用装饰器模式,仅需在运行时添加一个装饰器对象即可实现,可以实现最大的灵活性。