0%

设计模式初探

前言

设计模式作为一个程序员,相信大家肯定不会陌生,它是一些成熟的且通用的程序设计解决方案,针对这些肯定会存在一些理论基础,来为这些这些模式提供理论依据,这里我们就要先搞明白这些理论到底是什么,这样我们对设计模式有事半功倍的效果。

设计模式的基本原则:

  1. 单一职责原则
  2. 开闭原则
  3. 里式替换原则
  4. 依赖倒转原则
  5. 接口隔离原则
  6. 合成复用原则
  7. 迪米特法则

单一职责原则

定义:规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。

作用:它用于控制类的粒度大小

这个原则很好理解,我们的类在做某些事情的时候只专注与自己领域内的事儿就可以了,譬如我们的模型类就只针对特定模型进行操作,而不会去关心操作类里面的逻辑,这样单一的职责隔离,可以方便我们维护。

举个理想化例子:

现实生活中,我们的摄影师是什么都干的,布景、服装、灯光、拍照,可以说是累成狗。

但是在程序设计的世界里面,我们更加希望的是这样:

  • 我们的摄影师主要负责就是控制相机,指挥助手。
  • 指导助手布景,而具体的布景、服装和灯光布置可以交给我们助手。

我们用代码模拟下现实生活:

1
<?php
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
<?php
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
<?php
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
<?php
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
<?php
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
<?php
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
<?php
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
<?php
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
<?php
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
<?php
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
<?php
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 类图就最好了。