设计模式的六大原则

设计模式的六大原则

目录

单一职责原则

引言:

​ 说到单一职责原则很多都会不屑一顾,因为它 太简单 了,稍微有经验的程序员即使没有读过设计模式,没有听说过单一职责原则,在设计软件时也会自觉遵守这一重要原则。因为这是常识。

​ 在软件编程中,谁也不希望因为修改了一个功能导致其它功能发生故障。虽然单一原则如此简单,并被认定为常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。

​ 为什么会出现这种现象?

​ 因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和职责P2。

​ 比如:类T只负责职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的P1和P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为T1和T2,分别负责P1和P2两个职责。但在程序已经写好的情况下,这样做简直太浪费时间了。所以简单的修改类T,用它来负责两个职责是一个比较不错的选择。虽然这样做有悖于单一职责原则。

这样做的风险在于 职责扩展的不稳定性 ,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4 …… Pn 。

所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。

定义:

一个类只负责一项职责,不要存在多于一个导致类变更的原因。

实例:

  • 需求:用一个类描述动物呼吸这个场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

class Animal
{
public function breathe(string $animal)
{
echo "{$animal}呼吸空气";
echo "<br />";
}
}

$animal = new Animal();
$animal->breathe('牛');
$animal->breathe('羊');
$animal->breathe('猪');
  • 运行结果:
1
2
3
4
5
牛呼吸空气

羊呼吸空气

猪呼吸空气

程序上线之后,发现问题了,并不是所有动物都是呼吸空气的,例如:鱼是呼吸水的。

针对需求变更,有三种解决方法

  1. 遵循单一职责原则,需要将类 Animal 细分为陆生动物类 Terrestrial 和水生动物类 Aquatic

  2. 方法层面的单一职责原则,在类 Animal 中新增一个方法 breatheTwo 用于水生动物的呼吸。

  3. 代码层面的,直接修改 breathe 方法,判断变量 $animal 是什么种类的动物,执行对应的逻辑。

  • 方法一:遵循单一职责原则(推荐做法)
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
<?php

class Terrestrial
{
public function breathe(string $animal)
{
echo "{$animal}呼吸空气";
echo "<br />";
}
}

class Aquatic
{
public function breathe(string $animal)
{
echo "{$animal}呼吸水";
echo "<br />";
}
}

$terrestrial = new Terrestrial();
$terrestrial->breathe('牛');
$terrestrial->breathe('羊');
$terrestrial->breathe('猪');

$aquatic = new Aquatic();
$aquatic->breathe('鱼');

方法一 修改花销最大,不仅要修改类,还需要修改客户端。

  • 方法二:方法层面的单一职责原则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

class Animal
{
public function breathe(string $animal)
{
echo "{$animal}呼吸空气";
echo "<br />";
}

public function breatheTwo(string $animal)
{
echo "{$animal}呼吸水";
echo "<br />";
}
}

$animal = new Animal();

$animal->breathe('牛');
$animal->breathe('羊');
$animal->breathe('猪');
$animal->breatheTwo('鱼');

方法二这种修改方式没有改动原来的方法
而是在类中直接新增了一个方法
这样虽然也违背了单一职责原则
但是在方法级别上却是符合单一职责原则的

  • 方法三:代码级别的修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class Animal
{
public function breathe(string $animal)
{
if ($animal == '鱼') {
echo "{$animal}呼吸水";
echo "<br />";
} else {
echo "{$animal}呼吸空气";
echo "<br />";
}
}
}

$animal = new Animal();

$animal->breathe('牛');
$animal->breathe('羊');
$animal->breathe('猪');
$animal->breathe('鱼');

方法三修改起来最简单,隐患却是最大的。
有一天需要把鱼分为呼吸淡水的鱼和呼吸海水的鱼
则又需要修改Animal类的breathe方法
而对原有方法的修改会对调用“猪”“牛””羊”等相关功能带来风险
也许你某一天会发现程序运行就结果变为 “牛呼吸水了”

遵循单一职责原则的优点有:

  • 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多
  • 提高类的可读性,提高系统可维护性
  • 变更引起的风险最低,当修改一个功能时,可以显著降低对其他功能的影响。

需要说明一点的是,单一职责原则不只是面向对象编程思想所持有的,只要模块化的程序设计,都适用单一职责原则。

里氏替换原则

引言:

​ 继承作为面向对象三大特性之一,在给程序设计带来巨大的便利的同时,也带来了弊端。

​ 比如使用继承会给程序带来入侵性,程序的可移植性降低,增加了对象的耦合性,如果一个类被其他类所继承,则当这个类需要修改时,必须考虑到所有的子类,而且父类修改后,所有涉及到子类的功能都有可能会产生故障。

​ 继承包含这样一层含义:父类中凡是实现好的方法(相对于抽象方法而言),实际上是在设定一定系列的规范和契约,虽然它不强制要求所有子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个抽象体系造成破坏。而历史替换原则就是表达了这一层含义。

定义:

子类可以扩展父类的功能,但不能改变父类原有的功能。

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
  • 子类可以增加自己特有的方法
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类的输入参数更宽松
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格

依赖倒置原则

引言:

​ 依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节搭建起来的架构要稳定的多。

​ 抽象指的是接口或者抽象类;细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现的细节的任务交给他们的实现类去完成。

​ 依赖倒置原则的核心思想是 面向接口编程

定义:

高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不依赖细节;细节应该依赖抽象。

实例:

  • 需求:母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class Book
{
public function getContent()
{
return "很久很久以前有一个阿拉伯的故事......";
}
}

class Mother
{
public function narrate(Book $book)
{
echo '妈妈开始讲故事:' . $book->getContent();
}
}

$mother = new Mother();
$book = new Book();
$mother->narrate($book);
  • 运行结果:
1
妈妈开始讲故事:很久很久以前有一个阿拉伯的故事......

假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸代码如下:

1
2
3
4
5
6
7
class Newspaper
{
public function getContent()
{
return "林书豪38+7领导尼克斯击败湖人";
}
}

这位母亲居然办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改 Mother 类才能读。

假如以后需求换成杂志呢?换成网页呢?还要不断修改 Mother 类,这显然不是好的设计。原因是 MotherBook 之间的耦合性太高了,必须降低他们之间的耦合度才行。

  • 解决方法:我们引入一个抽象的接口 IReader 。读物,只要是带字的都属于读物,每种读物都有自己的名称。
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
<?php

interface IReader
{
public function getContent();

public function getName();
}

class Book implements IReader
{
public function getContent()
{
return "很久很久以前有一个阿拉伯的故事......";
}

public function getName()
{
return "故事";
}
}

class Newspaper implements IReader
{
public function getContent()
{
return "林书豪38+7领导尼克斯击败湖人";
}

public function getName()
{
return "报纸";
}
}

class Mother
{
public function narrate(IReader $iReader)
{
echo '妈妈开始讲' . $iReader->getName() . ':' . $iReader->getContent();
echo '<hr>';
}
}

$mother = new Mother();
$book = new Book();
$newspaper = new Newspaper();
$mother->narrate($book); // 故事书
$mother->narrate($newspaper); // 报纸
  • 运行结果:
1
2
妈妈开始讲故事:很久很久以前有一个阿拉伯的故事......
妈妈开始讲报纸:林书豪38+7领导尼克斯击败湖人

这样修改后,以后无论怎么扩展客户端,都不需要修改 Mother 类。这只是一个简单的例子。

代表高层模块的 Mother 类负责完成主要的业务逻辑,一旦需要对它进行修改,引入的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

采用依赖倒置原则给多人并行开发带来了极大的便利,比如上例中,原本 Mother 类于 Book 类直接耦合时,Mother 类依赖于 Book 类。修改后的程序则可以同时开工,互不影响,因为 MotherBook 类一点关系也没有。参与开发的人越多、项目越庞大,采用依赖倒置原则的意义就越重大。现在很流行的 TDD 开发模式就是依赖倒置原则最成功的应用。

传递依赖关系有三种方式,以上例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和 setter 方法传递。

在实际编程中,我们一般需要做到如下3点:

  • 低层模块尽量要有抽象类或者接口,或者两者都有
  • 变量的声明类型尽量是抽象类或接口
  • 使用继承时遵循 里氏替换原则

依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。

接口隔离原则

引言:

​ 很多人会觉得接口隔离原则和之前的单一职责原则很相似,其实不然。其一,单一职责原则注重的是职责;而接口隔离注重对接口依赖的隔离。其二,单一职责主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离主要约束接口,主要针对抽象,针对程序整体框架的构建。

定义:

​ 建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽可能少。

​ 我们要每个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

迪米特法则

引言:

​ 自从我们接触编程开始,就知道了软件编程总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。

​ 迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但凡事有有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个『 中介 』,过分的使用迪米特法则,会产生大量的中介和传递类,导致系统复杂度变大。

定义:

​ 一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外提供 public 方法,不对外泄露任何信息。

开闭原则

引言:

​ 在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

​ 开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来进行扩展。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

定义:

​ 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

总结

​ 开闭原则无非就是表达了这样一层意思:用抽象构建框架,用实现扩展细节

​ 因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。

​ 而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要变化时。我们只需要根据需求重新派生一个实现类扩展就可以了。

​ 当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行

​ 回想前面说的几项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合;而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

关键词 : 抽象 - 接口 - 面向接口编程 - 封闭 - 不允许修改; 修改 - 接口派生的实现类 - 扩展 - 根据需求随时可可变;

感谢您的阅读,本文由 Double-c 版权所有。如若转载,请注明出处:Double-c(https://double-c.github.io/2019/01/28/php-design-six-rule/
你是怎么变自律?
php设计模式范例