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