设计模式是一套被反复使用、多数人知晓、经过分类编目的、代码设计经验的总结。使用设计模式是为了 提高代码复用性 和 灵活性,让代码更容易被他人理解、保证代码 可靠性。
为了实现代码的 可复用性 和 灵活性。设计模式 提出了一些关键的 面向对象设计原则。
单一职责
其核心思想为:一个类,最好只做一件事,应该仅有一个引起它变化的原因。
可以理解为,一个类,应该是一组 相关性很高 的方法及数据的封装。
当一个类承担的职责过多时,就相当于把这些职责耦合在了一起,当其中一个职责发生变动,可能会对其他职责造成影响。
类的职责包括两个方面,数据职责和行为职责,数据职责通过类的属性实现,行为职责通过其方法实现。
单一职责是实现高内聚、低耦合的指导方针。它是最简单但又最难实现的原则,需要开发人员发现类的的不同职责并将其分离。
举个🌰:登陆模块显示登录页面,校验登录参数,连接数据库,查找用户,返回结果。
功能太过耦合,拆分成多个模块。
开闭原则
开闭原则是面向对象中最重要的原则。
一个软件应当对扩展开放,对修改关闭。也就是说在设计一个模块的时候,应该使这个模块可以在不被修改的前提下进行扩展。
一个类一旦开发完成,后续新增的功能就不应该通过修改这个类来完成,而是应该通过继承增加新的类。为什么要对修改关闭呢?因为一旦修改某个类,就可能破坏了系统原有功能,就需要重新测试。
抽象化 是开闭原则的关键。什么是抽象化,就是把一个或多个类中的公共的、有共性的东西抽取出来。抽象的最大好处在于它是抽象的、稳定的,不容易发生改变的。实现开闭原则的核心思想就是 面向接口编程,而不是具体实现。
开闭原则可以用一个更加具体的原则来描述:可变性封装。也就是找到系统中的可变因素并把它封装起来。
上一篇中提到的灯是个绝佳的🌰。
里氏替换
所有引用基类(父类)的地方必须能透明的使用其子类的对象。
这句话怎么理解呢?通俗来讲就是在软件中如果能够使用基类对象,那么一定也可以使用其子类对象。把基类都替换为它的子类对象,程序不会产生任何错误和异常。反过来则不成立。
里氏替换应该是开闭原则的一个扩展,由于使用基类对象的地方都可以使用其子类对象,因此在程序中尽量以基类类型来对对象进行定义,而在运行时再用其子类对象替换基类对象。
其中有一点很关键,里氏替换原则强调子类尽量使用基类中的方法,而不是重写,除非子类有其特殊性。
举个🌰,依然是上一篇提到的灯,但是加了一点改动:
| 12
 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
 
 | class Light{
 function show()
 {
 echo '灯光随机', PHP_EOL;
 }
 }
 
 class BlueLight extends Light
 {
 function show()
 {
 echo '蓝色', PHP_EOL;
 }
 }
 
 class RedLight extends Light
 {
 private bool $power = false;
 
 public function hasPower(): bool
 {
 return $this->power;
 }
 
 
 
 
 function show()
 {
 if(!$this->hasPower()){
 throw new Exception('没电了,亮不起来');
 }
 echo '红色', PHP_EOL;
 }
 }
 
 class User
 {
 function openLight(Light $light)
 {
 $light->show();
 }
 }
 
 $user = new User();
 $light = new Light();
 $blueLight = new BlueLight();
 $redLight = new RedLight();
 
 $user->openLight($light);
 $user->openLight($blueLight);
 $user->openLight($redLight);
 
 | 

根据里氏替换原则,子类必须能够替代父类。也就是说,虽然子类重写了父类的方法,但是在能够使用父类的场景里面,也一定要能够使用子类。很显然,BlueLight 类符合原则,RedLight 类虽然也实现了父类方法,但是抛出了父类没有的异常,所以违反了里氏替换原则,在 User 类中,我们无论是传入 Light 基类对象,还是 BlueLight 类对象,都是没有任何错误和异常的。
由以上实例,我们可以总结出,里氏替换原则本质是对继承的约束。
依赖倒置
依赖于抽象层,不依赖于具体。即高层次的模块不应该依赖低层次模块。
抽象不应该依赖于细节,细节应该依赖于抽象。
要面向接口编程,而不是面向实现编程。
一般情况下,我们认为调用者是高层模块,被调用者是底层模块。
实现开闭原则的关键是抽象化,如果说实现开闭原则是面向对象编程的目标,那么依赖倒置就是面向对象编程的主要手段。常用的手段为在代码中使用抽象类,将具体实现放入元数据。
再强调一遍,抽象是相对稳定的,不容易发生改变。
再举个🌰:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | class Book{
 public function getContent()
 {
 echo 'long long ago, 遥远的东方有一个特别英俊帅气的小伙', PHP_EOL;
 }
 }
 
 class Mother
 {
 public function narrate(Book $book)
 {
 $book->getContent();
 }
 }
 
 class Paper
 {
 public function getContent()
 {
 echo '上周五,中国首次实现经济反超美国称为世界第一经济体', PHP_EOL;
 }
 }
 $mother = new Mother();
 $mother->narrate(new Book());
 
 | 
在上边的例子中,麻麻看书讲故事,如果有一天,书看烦了,想看个报纸,但是麻麻做不到,因为要读报纸首先要把麻麻改掉。这就有点荒谬。显示不是一个好的设计,原因就是麻麻和书之间的耦合程度太高了,必须降低他们之间的耦合度。
我们来做下调整:
| 12
 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
 
 | interface Reader{
 public function getContent();
 }
 
 class Book implements Reader
 {
 public function getContent()
 {
 echo 'long long ago, 遥远的东方有一个特别英俊帅气的小伙', PHP_EOL;
 }
 }
 
 class Paper implements Reader
 {
 public function getContent()
 {
 echo '上周五,中国首次实现经济反超美国称为世界第一经济体', PHP_EOL;
 }
 }
 
 class Mother
 {
 public function narrate(Reader $reader)
 {
 $reader->getContent();
 }
 }
 
 $mother = new Mother();
 $mother->narrate(new Book());
 $mother->narrate(new Paper());
 
 | 
首先,我们抽取出一个接口类 Reader,然后 Book 和 Paper 分别去实现它。然后将 Mother 调整为依赖于接口。
现在,无论是想看书还是想看报纸,又或者想看连环画,我们只要去实现 Reader 即可,再也不用改动 Mother 了。
上边里氏替换中灯的例子其实已经符合依赖倒置原则,但是看到这个例子更生动,更容易理解,所以就赘述了一下。
传递依赖关系的方式有三种,上边的例子中使用的接口传递,还有构造函数传递和 setter 传递。
接口隔离
客户端不应该依赖它不需要的接口。
使用多个专门的接口,而不是一个大的单一的接口。
看上去似乎和单一职责很像,但是不是,单一职责针对的是类的职责,接口隔离针对的则是接口。
举个🌰:
| 12
 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
 
 | interface WorkerInterface{
 public function work();
 
 public function sleep();
 }
 
 class HumanWorker implements WorkerInterface
 {
 
 public function work()
 {
 echo 'I like working', PHP_EOL;
 }
 
 public function sleep()
 {
 echo 'I like sleeping', PHP_EOL;
 }
 }
 
 class RobotWorker implements WorkerInterface
 {
 public function work()
 {
 echo 'I like working', PHP_EOL;
 }
 
 public function sleep()
 {
 echo 'robot never sleep', PHP_EOL;
 }
 }
 
 | 
上边的例子中,有个很明显的缺点,机器人不需要睡觉,但是它却必须实现睡觉的方法,这显然违反了接口隔离。
那么,我们再调整一下:
| 12
 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
 
 | interface WorkInterface{
 public function work();
 
 }
 
 interface SleepInterface
 {
 public function sleep();
 }
 
 class HumanWorker implements WorkInterface, SleepInterface
 {
 
 public function work()
 {
 echo 'I like working', PHP_EOL;
 }
 
 public function sleep()
 {
 echo 'I like sleeping', PHP_EOL;
 }
 }
 
 class RobotWorker implements WorkInterface
 {
 public function work()
 {
 echo 'I like working', PHP_EOL;
 }
 }
 
 | 
调整后,接口一分为二,实际使用时各取所需,不再被迫实现自己不需要的接口。
合成复用
面向对象有两种方式实现代码复用,一是继承,二还是继承,哦不,而是组合/聚合,也可以叫做合成。
合成复用原则要求在软件复用时,首先考虑使用组合、聚合等关联方式实现,其次才考虑使用继承。也就是在一个新对象里通过关联方式使用已有对象的方法和功能。
那么为什么推荐使用组合呢,首先,继承后,父类的方法暴露给了子类,这等于破坏了类的封装性,所以继承复用也被称为白箱复用。其次,父类的方法发生变动会影响到子类,属于耦合度较高的一种表现,不利于代码的维护。最后,继承自基类的方法是静态的,限制了复用的灵活性。


上个图吧,例子挺清晰的。图片出处
emmm,前边反复提的灯的例子就不太符合合成复用原则了。
有兴趣的可以自己改造一下。
迪米特法则
迪米特法则又叫最少知道原则,它强调每一个类应当对其它类有尽可能少的了解,不和陌生人说话。也就是尽可能少的产生依赖。
A 和 B 产生交互,B 和 C 产生交互,A 只和 B 交互,不跟 C 玩。
这个法则就比较搞,分了狭义和广义,怎么来的没搞清楚,后边再补充。
狭义强调如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一类的某一个方法的话,可以通过第三者转发这个调用。
广义则是对封装的强调,也就是对信息隐藏的控制。方法封装在类的内部,调用者只需要调用并获取预期结果,不需要关注具体实现。
举个反面🌰:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | class MySchool{
 
 public function manager()
 {
 $myClass = new MyClass();
 foreach ($myClass->getClasses() as $class) {
 foreach ($class->getStudents() as $student) {
 echo $student->getName(), PHP_EOL;
 }
 }
 
 }
 }
 
 | 
在上边的例子中,School 类和 Class 类发生交互,但是也和 Student 类发生了交互,违反了最少知道原则。
再来个优化版:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | class MySchool{
 
 public function manager()
 {
 $myClass = new MyClass();
 foreach ($myClass->getClasses() as $class) {
 $class->manager();
 }
 
 }
 }
 
 | 
通过 Class 类中 manager 方法去获取学生信息,School与 Student 类的交互就被清除了,School 也不知道 Class 是怎么获取 Student 信息的,它只管调用。
emmm,还可以继续递进,Class manager 只管理自己的信息, 然后调用 Student manager。
| 12
 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
 
 | class MySchool{
 
 public function manager()
 {
 $myClass = new MyClass();
 $myClass->manager();
 }
 }
 
 class MyClass
 {
 private function getClasses(): array
 {
 return [
 ...
 ];
 }
 
 public function manager()
 {
 foreach ($this->getClasses() as $class) {
 $student = new MyStudent();
 $student->manager();
 }
 }
 }
 
 class MyStudent
 {
 private function getStudents(): array
 {
 return [
 ...
 ];
 }
 
 public function manager()
 {
 foreach ($this->getStudents() as $student) {
 echo $student->name, PHP_EOL;
 }
 }
 }
 
 |