前言
设计模式作为一个程序员,相信大家肯定不会陌生,它是一些成熟的且通用的程序设计解决方案,针对这些肯定会存在一些理论基础,来为这些这些模式提供理论依据,这里我们就要先搞明白这些理论到底是什么,这样我们对设计模式有事半功倍的效果。
设计模式的基本原则:
- 单一职责原则
- 开闭原则
- 里式替换原则
- 依赖倒转原则
- 接口隔离原则
- 合成复用原则
- 迪米特法则
单一职责原则
定义:规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。
作用:它用于控制类的粒度大小
这个原则很好理解,我们的类在做某些事情的时候只专注与自己领域内的事儿就可以了,譬如我们的模型类就只针对特定模型进行操作,而不会去关心操作类里面的逻辑,这样单一的职责隔离,可以方便我们维护。
举个理想化例子:
现实生活中,我们的摄影师是什么都干的,布景、服装、灯光、拍照,可以说是累成狗。
但是在程序设计的世界里面,我们更加希望的是这样:
- 我们的摄影师主要负责就是控制相机,指挥助手。
- 指导助手布景,而具体的布景、服装和灯光布置可以交给我们助手。
我们用代码模拟下现实生活:
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\SingleResponsibility; |
5 | |
6 | class RealPhotographer |
7 | { |
8 | private string $name; |
9 | |
10 | public function __construct(string $name) |
11 | { |
12 | $this->name = $name; |
13 | } |
14 | |
15 | // 沟通 |
16 | public function communicate(): void |
17 | { |
18 | echo $this->name . ' 在沟通' . PHP_EOL; |
19 | } |
20 | |
21 | // 布置场景 |
22 | public function layout() |
23 | { |
24 | echo $this->name . ' 在布置场景' . PHP_EOL; |
25 | } |
26 | |
27 | // 搭配服装 |
28 | public function matchingClothing() |
29 | { |
30 | echo $this->name . ' 搭配衣服' . PHP_EOL; |
31 | } |
32 | |
33 | // 调整灯光 |
34 | public function adjustTheLights() |
35 | { |
36 | echo $this->name . ' 调整灯光' . PHP_EOL; |
37 | } |
38 | |
39 | // 控制相机 |
40 | public function controlCamera() |
41 | { |
42 | echo $this->name . ' 控制相机' . PHP_EOL; |
43 | } |
44 | } |
我们可以看到一个摄影师负责了方方面面,俗话就是管的太宽了,我们要缩小粒度,让我们的摄影师只是专注拍照的本质,所以我们可以把 布置场景
、搭配服装
、调整灯光
交给助手去完成。我们来修改下这个方法,让特定的事儿给专业的人去完成。
我们修改下这个类:
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\SingleResponsibility; |
5 | |
6 | // 摄影师类 |
7 | class Photographer |
8 | { |
9 | private string $name; |
10 | |
11 | public function __construct(string $name) |
12 | { |
13 | $this->name = $name; |
14 | } |
15 | |
16 | // 控制相机 |
17 | public function controlCamera() |
18 | { |
19 | echo $this->name . " 操作控制相机拍照" . PHP_EOL; |
20 | } |
21 | |
22 | // 指挥助手 |
23 | public function commandAssistant(Helper $helper) |
24 | { |
25 | $helper->receivedCommand(); |
26 | $helper->matchingClothing(); |
27 | $helper->adjustTheLights(); |
28 | $helper->layout(); |
29 | } |
30 | } |
31 | |
32 | // 助手类 |
33 | class Helper |
34 | { |
35 | private string $name; |
36 | |
37 | public function __construct(string $name) |
38 | { |
39 | $this->name = $name; |
40 | } |
41 | |
42 | public function receivedCommand() |
43 | { |
44 | echo $this->name . '收到指挥' . PHP_EOL; |
45 | } |
46 | |
47 | // 布置场景 |
48 | public function layout() |
49 | { |
50 | echo $this->name . ' 在布置场景' . PHP_EOL; |
51 | } |
52 | |
53 | // 搭配服装 |
54 | public function matchingClothing() |
55 | { |
56 | echo $this->name . ' 搭配衣服' . PHP_EOL; |
57 | } |
58 | |
59 | // 调整灯光 |
60 | public function adjustTheLights() |
61 | { |
62 | echo $this->name . ' 调整灯光' . PHP_EOL; |
63 | } |
64 | } |
这样我们就把职责更加明确的分配了,其实程序员有时候更像是一个管理者的觉得,我们需要管理具体的类去做具体的事儿。在管理这些类的时候,我们要合理的划分这些类的职责,否则职责到后面越来越混乱,反而影响我们的管理。所以单一职责让我们能更好的控制类的粒度。
开闭原则
定义:一个软件实体应当对扩展开放,对修改关闭。
我们的软件随着时间推移是会发生一些变化的,但是已有的代码已经是稳定运行的,我们不应该去修改这些成熟的代码扩展他们的功能,除非逼不得已。所以这就考验到我们的设计水平了。
我们还是看看下面这个场景:
- 摄影师工作时不只是只用一个牌子的相机,不同的厂商的相机,有不同的效果
我们来看看我们通常专注于实现的代码是什么样子的:
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\OpenAndClose; |
5 | |
6 | class Photographer |
7 | { |
8 | private string $name; |
9 | |
10 | public function __construct(string $name) |
11 | { |
12 | $this->name = $name; |
13 | } |
14 | |
15 | public function photograph(string $camera) |
16 | { |
17 | switch ($camera) { |
18 | case '佳能': |
19 | echo $this->name . ' 使用佳能拍' . PHP_EOL; |
20 | break; |
21 | case '尼康': |
22 | echo $this->name . ' 使用尼康拍' . PHP_EOL; |
23 | break; |
24 | case '索尼': |
25 | echo $this->name . ' 使用索尼拍' . PHP_EOL; |
26 | break; |
27 | default: |
28 | echo $this->name . ' 使用手机拍' . PHP_EOL; |
29 | break; |
30 | } |
31 | } |
32 | } |
这里代码看着是实现了我们的需求,但是如果客户要求用 哈苏
、宾得
、富士
拍照呢?你是不是要去修改 photograph
这个方法。这样就违背了我们的开闭原则。那我们要怎么才能不修改代码的情况下,去完成我们的进击的需求呢?我们可以这样改:
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\OpenAndClose; |
5 | |
6 | interface Camera |
7 | { |
8 | function photograph(): string; |
9 | } |
10 | |
11 | class CannonCamera implements Camera |
12 | { |
13 | public function photograph(): string |
14 | { |
15 | return ' 使用佳能拍' . PHP_EOL; |
16 | } |
17 | } |
18 | |
19 | class NikonCamera implements Camera |
20 | { |
21 | public function photograph(): string |
22 | { |
23 | return ' 使用尼康拍' . PHP_EOL; |
24 | } |
25 | } |
26 | |
27 | class SonyCamera implements Camera |
28 | { |
29 | public function photograph(): string |
30 | { |
31 | return ' 使用索尼拍' . PHP_EOL; |
32 | } |
33 | } |
34 | |
35 | class FujiCamera implements Camera |
36 | { |
37 | public function photograph(): string |
38 | { |
39 | return ' 使用富士拍' . PHP_EOL; |
40 | } |
41 | } |
42 | |
43 | class Photographer |
44 | { |
45 | private string $name; |
46 | |
47 | public function __construct(string $name) |
48 | { |
49 | $this->name = $name; |
50 | } |
51 | |
52 | public function photograph(Camera $camera): void |
53 | { |
54 | echo $this->name . $camera->photograph(); |
55 | } |
56 | } |
57 | |
58 | $photographer = new Photographer("鱼不浪"); |
59 | $photographer->photograph(new CannonCamera()); |
60 | $photographer->photograph(new NikonCamera()); |
61 | $photographer->photograph(new SonyCamera()); |
62 | $photographer->photograph(new FujiCamera()); |
我们可以使用接口把相机的拍照功能抽象出来,这样即使是有新的相机进来,我们无非就是实现这个接口就能达到扩展的目的,而不需要去修改我们的现有代码。
里式替换原则
定义:所有引用基类的地方必须透明地使用其子类的对象。
使用里式替换原则时需要注意如下:
- 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
- 我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
其实还是一个抽象的概念,我们还是拿摄影师来说:
- 摄影师拿相机,至于什么牌子的相机我们不管,我们只是抽象相机这个概念
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\Liskv; |
5 | |
6 | abstract class Camera |
7 | { |
8 | public function open() |
9 | { |
10 | echo "相机开机" . PHP_EOL; |
11 | } |
12 | |
13 | public abstract function screen(): void; |
14 | } |
15 | |
16 | class CannonCamera extends Camera |
17 | { |
18 | public function screen(): void |
19 | { |
20 | echo "佳能拍照" . PHP_EOL; |
21 | } |
22 | } |
23 | |
24 | class NikonCamera extends Camera |
25 | { |
26 | public function screen(): void |
27 | { |
28 | echo "尼康拍照" . PHP_EOL; |
29 | } |
30 | } |
31 | |
32 | class Photographer |
33 | { |
34 | /** |
35 | * 这里参数是父类,我们可以传入子类 |
36 | * @param Camera $camera |
37 | */ |
38 | public function screen(Camera $camera) |
39 | { |
40 | $camera->open(); |
41 | $camera->screen(); |
42 | } |
43 | } |
44 | |
45 | $photographer = new Photographer(); |
46 | $photographer->screen(new CannonCamera()); |
47 | $photographer->screen(new NikonCamera()); |
依赖倒转原则
定义:抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
这个我们在开闭原则中已经给出了实例,我们就是针对上层抽象进行的编程。
我们来看看常用的三种注入方式:
- 构造注入
- 设值注入
- 接口传递注入
我们挨个看,为了省事儿我就只用一个类来演示:
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\Dependency; |
5 | |
6 | interface Camera |
7 | { |
8 | public function open(): void; |
9 | |
10 | public function screen(): void; |
11 | } |
12 | |
13 | // 实现相机接口 |
14 | class CannonCamera implements Camera |
15 | { |
16 | public function open(): void |
17 | { |
18 | echo "打开佳能相机" . PHP_EOL; |
19 | } |
20 | |
21 | public function screen(): void |
22 | { |
23 | echo "佳能相机拍照" . PHP_EOL; |
24 | } |
25 | } |
26 | |
27 | // 实现相机接口 |
28 | class NikonCamera implements Camera |
29 | { |
30 | public function open(): void |
31 | { |
32 | echo "打开尼康相机" . PHP_EOL; |
33 | } |
34 | |
35 | public function screen(): void |
36 | { |
37 | echo "尼康相机拍照" . PHP_EOL; |
38 | } |
39 | } |
40 | |
41 | class Photographer |
42 | { |
43 | private Camera $camera; |
44 | |
45 | // 使用构造注入 |
46 | public function __construct(Camera $camera) |
47 | { |
48 | $this->camera = $camera; |
49 | } |
50 | |
51 | /** |
52 | * 使用设值注入 |
53 | * @param Camera $camera |
54 | */ |
55 | public function setCamera(Camera $camera): void |
56 | { |
57 | $this->camera = $camera; |
58 | } |
59 | |
60 | /** |
61 | * 使用接口传递注入 |
62 | * @param Camera $camera |
63 | */ |
64 | public function open(Camera $camera): void |
65 | { |
66 | $camera->open(); |
67 | } |
68 | |
69 | public function screen() |
70 | { |
71 | $this->open($this->camera); |
72 | $this->camera->screen(); |
73 | } |
74 | } |
75 | |
76 | // 我们摄影师本来有自己佳能相机 |
77 | $photographer = new Photographer(new CannonCamera()); |
78 | // 并用它进行拍照 |
79 | $photographer->screen(); |
80 | |
81 | // 朋友带着尼康相机来了,他拿朋友的尼康相机来玩儿 |
82 | $photographer->setCamera(new NikonCamera()); |
83 | $photographer->screen(); |
接口隔离原则
定义:
- 客户端不应该依赖它不需要的接口。
- 类间的依赖关系应该建立在最小的接口上。
我们通过上面的例子,发现接口真的是一个好东西,可以让我们解耦很多我们的程序。
还是摄影师的例子,不同摄影师有不同的行为,我们不排除有的摄影师啥都 OK,有的摄影师也缺乏一些能力。
- 摄影师可以拍摄表达作品想法
- 摄影师也要和客户沟通,但有的摄影师只是工具人,不需要沟通
- 有的摄影师自己重洗胶片,有的直接用数码
对于这些接口我们的摄影师不用都实现,实现自己需要的接口就好了。
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\InterfaceSegregation; |
5 | |
6 | interface Screen |
7 | { |
8 | public function screen(); |
9 | } |
10 | |
11 | interface Communicate |
12 | { |
13 | public function communicate(); |
14 | } |
15 | |
16 | interface RinseTheFilm |
17 | { |
18 | public function rinseTheFilm(); |
19 | } |
20 | |
21 | class PhotographerOne implements Screen, Communicate |
22 | { |
23 | public function screen() |
24 | { |
25 | echo "PhotographerOne 拍照" . PHP_EOL; |
26 | } |
27 | |
28 | public function communicate() |
29 | { |
30 | echo "PhotographerOne 沟通" . PHP_EOL; |
31 | } |
32 | } |
33 | |
34 | class PhotographerTwo implements Screen, Communicate, RinseTheFilm |
35 | { |
36 | public function screen() |
37 | { |
38 | echo "PhotographerTwo 拍照" . PHP_EOL; |
39 | } |
40 | |
41 | public function communicate() |
42 | { |
43 | echo "PhotographerTwo 沟通" . PHP_EOL; |
44 | } |
45 | |
46 | public function rinseTheFilm() |
47 | { |
48 | echo "PhotographerTwo 冲洗照片了" . PHP_EOL; |
49 | } |
50 | } |
合成复用原则
定义:尽量使用合成、聚合的方式,而不是使用继承
这里有很多种情况,还是用代码来举例说明,我们在有时候在开发中,有时候类可能会用到别的类的方法,我们可以有继承,依赖,聚合,组合的关系。继承会导致我们的程序耦合度过高,所以我们会选择另外的三种方式:
- 依赖
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\Composite; |
5 | |
6 | class CompositeParent |
7 | { |
8 | public function method1() |
9 | { |
10 | echo "方法1" . PHP_EOL; |
11 | } |
12 | |
13 | public function method2() |
14 | { |
15 | echo "方法2" . PHP_EOL; |
16 | } |
17 | |
18 | public function method3() |
19 | { |
20 | echo "方法3" . PHP_EOL; |
21 | } |
22 | } |
23 | |
24 | class CompositeChild |
25 | { |
26 | public function method1(CompositeParent $compositeParent) |
27 | { |
28 | $compositeParent->method1(); |
29 | } |
30 | } |
- 聚合
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\Composite; |
5 | |
6 | class CompositeParent |
7 | { |
8 | public function method1() |
9 | { |
10 | echo "方法1" . PHP_EOL; |
11 | } |
12 | |
13 | public function method2() |
14 | { |
15 | echo "方法2" . PHP_EOL; |
16 | } |
17 | |
18 | public function method3() |
19 | { |
20 | echo "方法3" . PHP_EOL; |
21 | } |
22 | } |
23 | |
24 | class CompositeChild |
25 | { |
26 | private CompositeParent $compositeParent; |
27 | |
28 | public function setCompositeParent(CompositeParent $compositeParent): void |
29 | { |
30 | $this->compositeParent = $compositeParent; |
31 | } |
32 | } |
- 组合
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\Composite; |
5 | |
6 | class CompositeParent |
7 | { |
8 | public function method1() |
9 | { |
10 | echo "方法1" . PHP_EOL; |
11 | } |
12 | |
13 | public function method2() |
14 | { |
15 | echo "方法2" . PHP_EOL; |
16 | } |
17 | |
18 | public function method3() |
19 | { |
20 | echo "方法3" . PHP_EOL; |
21 | } |
22 | } |
23 | |
24 | class CompositeChild |
25 | { |
26 | private CompositeParent $compositeParent; |
27 | |
28 | public function __construct() |
29 | { |
30 | $this->compositeParent = new CompositeParent(); |
31 | } |
32 | } |
迪米特法则
定义:
- 一个对象应该对其他对象保持最少的了解
- 类与类关系越密切,耦合度越大
- 一个类对自己依赖的类知道的越少越好,对于被依赖的类不管多复杂,都尽量将逻辑封装在内部。对外部除了提供 public 方法,不要透露任何信息
- 只与直接的朋友通信
- 直接朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、聚合等。其中我们称出现成员变量,方法参数,方法返回值中的类为直接朋友,而局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
1 |
|
2 | declare(strict_types=1); |
3 | |
4 | namespace Neilyoz\DesignPatternBase\LawOfDemeter; |
5 | |
6 | use phpDocumentor\Reflection\Types\This; |
7 | |
8 | class Student |
9 | { |
10 | |
11 | } |
12 | |
13 | class Police |
14 | { |
15 | |
16 | } |
17 | |
18 | class LawOfDemeter |
19 | { |
20 | // 直接朋友 |
21 | private Student $student; |
22 | |
23 | public function getStudent(): Student |
24 | { |
25 | return $this->student; |
26 | } |
27 | |
28 | public function setStudent(Student $student): void |
29 | { |
30 | $this->student = $student; |
31 | } |
32 | |
33 | public function doSomething() |
34 | { |
35 | // 非直接朋友 |
36 | $police = new Police(); |
37 | } |
38 | } |
总结
根据这些基本的原则去理解设计模式,感觉不会太困难,当然会 UML 类图就最好了。