dream

一个菜鸟程序员的成长历程

0%

设计模式之适配器模式–不兼容变兼容接口

适配器模式可以增加接口的易用性,使得不兼容的接口变得兼容。

将一个接口转换成另外的接口,使得原本不兼容的接口变得兼容。

为什么要使用适配器模式

比如你的接口设计存在缺点,不够易用,不能复用但又希望复用的时候。

比如你要对接多个平台的接口他们互不兼容的时候。

现实中也有很多适配器,比如安卓苹果的转换头,比如usb到type-c的转换头,都是因为两边的接口不同,不兼容所以需要一个适配器。

下面是一个手机类,现在需要充电,但是手机的充电需要typec接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 适配器模式
* 手机类
*/
class mobile {

/**
* 给手机充电操作
*/
public function charge(ITypec $typec) {
dump('使用typec接口给手机充电');
$typec->typec();
}

}

现在只有一个usb接口可以充电。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 适配器模式
* 需要适配的类
* usb接口
*/
class usb {

/**
* 接口
*/
public function usb() {
dump('电脑usb接口');
}

}

我们增加一个适配器,也就是typec转usb的头。

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
/**
* 适配器模式
* 适配器接口
* typec接口
*/
interface ITypec {
/**
* 接口
*/
public function typec();

}

/**
* 适配器模式
* 适配器类,typec转换usb的适配器
* typec接口
*/
class typec implements ITypec {

private $usb;

function __construct() {
$this->usb = new usb;
}

/**
* 接口
*/
public function typec() {
dump('接口适配器');
$this->usb->usb();
}

}

我们的客户端就可以使用这个适配器了。

1
2
3
4

$mobile = new mobile();

$mobile->charge(new typec);

这是对象适配器,还有类适配器,类适配器的话需要继承要适配的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/**
* 适配器模式
* 适配器类,typec转换usb的适配器
* typec接口
*/
class typec extends usb implements ITypec {

//不再需要存储对象,直接继承了

/**
* 接口
*/
public function typec() {
dump('接口适配器');
//$this->usb->usb();
//这里改成使用父类的usb方法。
parent::usb();
}

}

适配器和代理模式还有装饰器模式的区别

这几个设计模式其实都差不多,你也可以说使用了typec代理了usb。只是角度不同所以名字不同罢了。

  • 代理模式 更注重代理,代理了原有类。
  • 装饰器模式 比代理模式更加灵活多样。
  • 适配器模式 更注重兼容性,为了兼容别的类,而不是代理和装饰功能。

虽然角度不同,但是我觉得核心都差不多。这几个设计模式都是需要修改原有类的时候,没有选择修改,而是套了壳子,来扩展功能。这是一种好的思想,灵活运用思想更重要。

代码放在了我的github上面。

设计模式之装饰器模式–各种功能随意扩展

装饰器模式是灵活给原有类装饰新功能。

动态的给对象添加额外职责,就增加功能来说,装饰模式比生成子类更为灵活。

为什么要使用装饰器模式

如果你需要添加一些额外的功能,再不修改原有类的基础上,给原有类套个壳。并且可以套多个壳,我觉得装饰器模式有点像中间件。

比如你现在有一个登录的功能。

你现在需要给登录加一个权限控制。然后还要增加一个登录日志。还要增加登录微信通知。如果修改原有类可能引起未知bug。

可以使用装饰器模式。

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
/**
* 装饰器模式
* 装饰器父类
*/
class Decorate {
function __construct($compent) {
$this->compent = $compent;
}

function login() {
$this->compent->login();
}
}

/**
* 装饰器模式
* 权限装饰器
*/
class LoginAuth extends Decorate {
function login() {
dump('判断登录权限');
parent::login();
}
}

/**
* 装饰器模式
* 登录日志装饰器
*/
class LoginLog extends Decorate {
function login() {
parent::login();
dump('记录登录日志');
}
}


/**
* 装饰器模式
* 微信通知装饰器
*/
class LoginWechat extends Decorate {
function login() {
parent::login();
dump('发送微信登录通知');
}
}

下面是原来的登录功能

1
2
3
4
5
6
7
8
9
10
11
/**
* 装饰器模式
* 核心代码
* 登录功能
*/
class login {
function login() {
dump('用户登录');
}
}

原来的客户端直接调用login,只有登录功能。

1
2
$login = new login();
$login->login();

增加了装饰器以后可以把登录功能放入装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
$login = new login;

//使用权限装饰器
$login = new LoginAuth($login);

//使用登录日志装饰器
$login = new LoginLog($login);

//使用微信通知装饰器
$login = new LoginWechat($login);

$login->login();

decorate

还可以任意调换装饰器的位置,可以先发送微信通知在记录登录日志。

1
2
3
4
5
6
7
8
9
10
11
12
13

$login = new login;

//使用微信通知装饰器
$login = new LoginWechat($login);

//使用登录日志装饰器
$login = new LoginLog($login);

//使用权限装饰器
$login = new LoginAuth($login);

$login->login();

decorate

这个模式就非常灵活,可以任意组装自己的装饰,比如购买支付的时候,需要装饰积分发放,账单通知这些。而且每个装饰器的职责单一,方便插拔,如果不需要一个装饰器的时候把这个装饰器去掉就可以了。

代码放在了我的github上面。

设计模式之桥接模式–组合优于继承的体现

桥接模式是灵活运用组合对象而不是继承类来实现功能。

将抽象与实现部分分离,使他们都可以独立变化。

为什么要使用桥接模式

继承这个面向对象的特性使得我们继承父类可以继承父类的功能,在使用多态特性可以自由修改子类,使得我们的设计很好用。

但是继承有一个问题,就是强耦合。继承一个父类代表拥有这个父类的能力,但这个父类的很多能力你可能并不需要。

还有如果继承关系很复杂的时候,使用继承会导致关系复杂到破裂,无法继承。

比如华为手机和小米手机都具有玩游戏,听音乐等功能。按照继承设计就会有下面4个类,如果再实现拍照功能又需要增加2个类。这个继承关系太过复杂。

  • 手机父类

  • 软件父类

  • 华为游戏手机

  • 华为音乐手机

  • 小米游戏手机

  • 小米音乐手机

如果使用组合的方式,只有4个类。

  • 华为手机类
  • 小米手机类
  • 游戏类
  • 音乐类

让这4个类互相组合就可以了。如果增加个拍照,只需要增加拍照类。这就是抽象和实现分离。手机类就是手机类,软件类就是软件类,互相组合实现功能而不是互相继承。

桥接模式可以理解成连接两个部分,比如连接手机和软件两个部分。像一座桥一样。

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
/**
* 桥接模式
* 桥的一端
* 手机类
*/
abstract class Mobile {
protected $soft;
/**
* 运行手机功能
*/
public abstract function run();

/**
* 设置相应的软件
*/
public function setSoft(Soft $soft) {
$this->soft = $soft;
}

}

/**
* 桥接模式
* 桥的一端
* 软件类
*/
abstract class Soft {

/**
* 运行软件功能
*/
public abstract function run();


}

下面是手机类的具体实现和软件的具体实现。

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

/**
* 桥接模式
* 桥的一端
* 华为手机类
*
*/
class HuaWei extends Mobile {

/**
* 运行手机功能
*/
public function run() {
dump('华为手机');
$this->soft->run();
}

}


/**
* 桥接模式
* 桥的一端
* 小米手机类
*
*/
class XiaoMi extends Mobile {

/**
* 运行手机功能
*/
public function run() {
dump('小米手机');
$this->soft->run();
}

}


/**
* 桥接模式
* 游戏软件类
*/
class Game extends Soft{

/**
* 运行软件功能
*/
public function run() {
dump('玩王者荣耀');
}


}

/**
* 桥接模式
* 音乐软件类
*/
class Music extends Soft{

/**
* 运行软件功能
*/
public function run() {
dump('打开网易云音乐');
}


}

客户端调用根据需要注入不同的软件就可以实现不同的软件功能而不再需要增加新的类来实现。

1
2
3
4
5
6
7

$obj1 = new HuaWei;
$obj1->setSoft(new Game);
$obj1->run();

$obj1->setSoft(new Music);
$obj1->run();

bridge

桥接模式解决了继承过于复杂的问题,体现了组合优于继承的思想。

代码放在了我的github上面。

设计模式之代理模式–代理转发中间层

代理模式是代理原来的类或对象,在不改变原有类或对象的基础上增加新的功能。

为其他对象提供一种代理以控制对这个对象的访问。

为什么要使用原型模式

代理模式最常用的场景比如我们开发的代理中间层,来代理前端的请求,隐藏真正的后端接口。还有很多远程代理,科学上网也属于代理的一种。

还有虚拟代理,比如浏览器的图片会先出现图片框在加载图片,图片框其实就是一个对图片的代理。

安全代理,用来控制真实对象的访问权限,这个也是代理中间层会做到的。

在我们的代码中一般会使用代理模式做一些原有类做不到的事情,对原有类的扩展。

比如打印sql打印日志发送通知这些跟具体业务无关的功能增加。

这也更加符合开闭原则。通过增加代理类的方式来增加功能而不是修改原来的类。

代理模式要求代理类原类都实现同一个接口,也是对接口实现才能实现用代理类来替换原类。

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

//代理接口,代理类和原类都需要实现这个接口
interface IProxy{
public function proxy();
}

//代理类,实现代理接口,代理原类,客户端访问代理类
class proxy implements IProxy {
private $proxy;

function __construct(IProxy $proxy) {
//把要代理的原类传进来
$this->proxy = $proxy;
}

public function proxy() {
//前置功能

//访问原类
$this->proxy->proxy();

//后置功能

}
}

//原类
class product implements IProxy{
function proxy() {
//原类的功能
}
}

原来没有代理的时候,直接调用原类。

1
2
3
4

$product = new product();
$product->proxy();

有了代理类之后,客户端调用代理类,隐藏原类。

1
2
$proxy = new Proxy(new product());
$proxy->proxy();

其实就是套了一层壳。

代码放在了我的github上面。

设计模式之原型模式–打印机快速复制的原理

原型模式是一个克隆模式,以一个原型进行克隆,复制。

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

为什么要使用原型模式

因为原型模式可以克隆整个对象而不用重新生成。

如果有一个订单对象,你要是重新生成,需要再次查询数据库,这是一个很耗时的操作,如果直接复制就不需要耗时了。

在php里面实现很简单,每个类有__clone魔术方法,实现这个方法就好了。

先看不实现这个方法的克隆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 原型模式
* 实现__clone魔术方法
*/
class Prototype {

public $name = '张三';
public $arr = ['1', '2'];

}

$obj1 = new PrototypeClass;
dump($obj1);

$obj2 = clone $obj1;
$obj2->name = '456';
dump($obj2);

原型模式

可以看到成功克隆过来了,两个对象互不影响。

我们看一下修改第二个对象的arr属性呢

1
2
3
4
5
6
7
8
9

$obj1 = new PrototypeClass;
dump($obj1);

$obj2 = clone $obj1;
$obj2->name = '456';
$obj2->arr = ['1'];
dump($obj2);

原型模式

修改数组也没问题,也就是说,直接使用clone就可以完成深复制的拷贝操作。php本身通过clone关键字完成了原型模式。

代码放在了我的github上面。

设计模式之建造者模式–比工厂更精细的流水线生成

建造者模式是创建一个稳定流程的复杂对象,隐藏创建的具体流程、过程、细节。

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

为什么要使用建造者模式

当你需要创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化

建造者分离了构建和表示,他把如何创建隐藏了起来,你只要告诉建造者你需要什么,他就会给你建造什么。

建造者模式和工厂模式有什么区别

工厂模式更像一个大工厂,比如口罩工厂负责生产雾霾口罩防毒口罩等等。根据你需要的类型来判断给你什么

雾霾口罩怎么生产呢?假设需要123三个步骤,那么建造者就是负责这三个步骤的,你告诉建造者你需要雾霾口罩,那么他会给你一个完整的,建造好的雾霾口罩而不是口罩半成品

所以建造者更像一个完整的流水线,按照步骤建造好成品给你。

再比如你去饭店点餐,工厂模式是你点哪个菜就给你哪个菜。建造者模式是你点的菜怎么做。你点了鱼香肉丝,建造者会按照步骤,哪一步该放肉,哪一步该放盐,最后做好了把菜给你。

建造者模式还有一个作用就是约束。抽象出的建造流程不可改变,口罩不能少个过滤网,饭菜不能没有放盐。

建造者模式也避免了复杂对象创建流程的缺失。

php实现建造者模式

假设现在有一个对接第三方支付的需求。使用建造者模式。

我们要先抽象出一个建造流程。使用抽象类,对接第三方支付大概需要一个加密,一个读取配置信息,然后把这些参数合并发送出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

<?php
/**
* 建造者模式
* 建造者抽象类
* 抽象出整个建造流程
*/
abstract class Build {

/**
* 加密流程
*/
public abstract function buildHash();

/**
* 读取配置流程,比如appid这些
*/
public abstract function buildConfig();

}

接下来我们的第三方支付类继承这个抽象建造者来实现具体的第三方支付建造者。

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
/**
* 建造者模式
* 建造者抽象类
* 抽象出整个建造流程
*/
class ThreadBuild extends Build{

/**
* 加密流程
*/
public function buildHash() {
//省略加密代码
return 'hash';
}

/**
* 读取配置流程,比如appid这些
*/
public function buildConfig() {
//省略配置代码
return 'config';
}

/**
* 发送http请求流程
*/
public function http() {
//省略发送代码
return 'success';
}
}

有了建造者,我们现在需要一个组装这整个建造流程的监工指挥者。来保证整个流程不出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 建造者模式
* 指挥者类
* 按照流程建造,返回建造后的类。
*/
class Director{
static function createBuild(Build $build) {
$build->buildHash();
$build->buildConfig();
return $build;
}
}

我们的客户端调用。

1
2
3
4

$threadPay = Director::createBuild(new ThreadBuild());
$threadPay->http();

如果接入了别的第三方支付,那么我们只需要增加具体建造者就可以了,如果和抽象建造者冲突,那么说明没有抽象好。在客户端只需要更改具体建造者就可以了。

代码放在了我的github上面。

设计模式之工厂方法模式–更加符合开闭原则的工厂模式

工厂方法模式是简单工厂模式的升级版本,更加符合开闭原则。

定义一个创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

为什么要使用工厂方法模式

之前说了简单工厂模式简单工厂模式的工厂类是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

//简单工厂类
class factory{
function static createPay() {
if (小程序) {
//小程序支付
$pay = new miniWechatPay;
} else if (公众号) {
//公众号支付
$pay = new jsAPIPay;
} else if (pc) {
//扫码支付
$pay = new nativePay;
}
return $pay;
}
}

这样的工厂类是违反了开闭原则的,如果需要增加支付方法就需要修改这个工厂类,工厂方法模式就是解决这个问题的,使用工厂方法模式后不需要修改工厂类,只需要新增工厂类

改成工厂方法模式

为了符合开闭原则,我们需要创建多个工厂类和一个工厂接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

interface IFactory{
static function createPay();
}

class miniFactory implements IFactory{
static function createPay() {
return new miniWechatPay;
}
}

class jsAPIFactory implements IFactory{
static function createPay() {
return new jsAPIPay;
}
}

class nativeFactory implements IFactory{
static function createPay() {
return new nativePay;
}
}

好了,我们把实例化放到了这些子类中,如果增加实例化的需求只需要增加工厂类就可以了。这就是工厂方法模式。

相比简单工厂来说,这有一个缺点就是需要修改的时候虽然不需要修改工厂类了,但是需要修改客户端了。

下面的是原来的客户端。

1
2
3
4
5
6
7

//微信支付方法 省略了类
function wechatPay() {
//...省略
$pay = factory::createPay();
$pay->pay();
}

下面的是工厂方法模式的客户端

1
2
3
4
5
6
7
8

//微信支付方法 省略了类
function wechatPay() {
//...省略
$pay = miniFactory::createPay(); //需要修改这里
$pay->pay();
}

如果不想修改客户端也可以再增加一个简单工厂类来实例化工厂方法模式的类。

1
2
3
4
5
6
7
8
9
class factory{
static function createFactory() {
if (小程序) {
return miniFactory::createPay();
}
//省略其他判断和实例化
}
}

但是我觉得这样还不如直接用简单工厂来的好。

总的来说,简单工厂和工厂方法模式各有优缺点吧,不过对于php来说我觉得简单工厂就足以,虽然有修改代码的风险。但是也还好,而且通过封装成数组来实例化也不需要这一堆if-else。

代码放在了我的github上面。

解决composer-npm等包管理工具下载失败的问题

使用composer npm进行下载依赖包的时候经常需要翻墙。不过国内已经有了很多国内源来加速下载,比如阿里云的国内镜像等。

不过有时候国内镜像并不能解决问题。

还有的时候就算你挂了全局翻墙也不行。

这是因为你使用命令行的时候,是用不到翻墙工具的。

如果你有翻墙工具,那么可以在命令行上面设置一下,使用代理就可以了。

在gitbash上面的设置。

export http_proxy=http://127.0.0.1:41091 //这个端口是你本地代理工具提供的端口
export https_proxy=http://127.0.0.1:41091 //这个端口是你本地代理工具提供的端口

在cmd上的设置

set http_proxy=http://127.0.0.1:41091 //这个端口是你本地代理工具提供的端口
set https_proxy=http://127.0.0.1:41091 //这个端口是你本地代理工具提供的端口

设计模式之简单工厂模式–利用工厂解耦实例化对象

简单工厂模式是最常被提起的一个设计模式,他的意思是

利用简单工厂来决定实例化哪个类,而不是由外部程序来决定,把创建对象的操作内聚,解耦到工厂类中。

为什么要使用简单工厂模式

工厂模式除了简单工厂模式还有工厂方法模式,抽象工厂模式

简单工厂模式有什么好处呢?

简单,非常简单。并且拥有工厂模式的特性,解耦对象的生成。

如果不使用工厂模式,那么对象的创建散落在程序的各个地方,如果需要修改,那么很麻烦。

我们在设计类的时候,为了遵循单一职责原则,我们应该把类划分的尽可能单一,拿一个实际应用例子来说,支付场景。

支付场景

我们需要在小程序端使用小程序支付,公众号端使用h5支付,pc端使用扫码支付。

后端统一提供支付接口。代码类似下面这样。

1
2
3
4
5
6
7
8
9
10
function wechatPay() {
//...省略
if (小程序) {
//小程序支付
} else if (公众号) {
//公众号支付
} else if (pc) {
//扫码支付
}
}

上面的是一个伪代码,这样写不管扩展性,维护性都不好,更不符合单一职责,所以我们应该把支付逻辑抽出来。

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
interface pay{
function pay();
}

class miniWechatPay implements pay{
function pay() {
//调用小程序支付
}
}

class jsAPIPay implements pay{
function pay() {
//调用公众号,jsapi支付
}
}

class nativePay implements pay{
function pay() {
//调用扫码支付
}
}

//微信支付方法 省略了类
function wechatPay() {
//...省略
if (小程序) {
//小程序支付
$pay = new miniWechatPay;
$pay->pay();
} else if (公众号) {
//公众号支付
$pay = new jsAPIPay;
$pay->pay();
} else if (pc) {
//扫码支付
$pay = new nativePay;
$pay->pay();
}
}

在if判断里面有重复的地方,都是调用pay方法,我们可以抽出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//微信支付方法 省略了类
function wechatPay() {
//...省略
if (小程序) {
//小程序支付
$pay = new miniWechatPay;
} else if (公众号) {
//公众号支付
$pay = new jsAPIPay;
} else if (pc) {
//扫码支付
$pay = new nativePay;
}

$pay->pay();
}

如果接下来我们还要增加刷脸支付等等其他支付怎么办呢,我们只能修改这个方法,这显然是不对的,违反了开闭原则

增加支付其实和这个业务逻辑不是一个紧耦合的,我们应该增加他的复用性,如果我们要在其他地方支付呢。应该把实例化这部分也抽出来。就形成了简单工厂类

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

//简单工厂类
class factory{
function static createPay() {
if (小程序) {
//小程序支付
$pay = new miniWechatPay;
} else if (公众号) {
//公众号支付
$pay = new jsAPIPay;
} else if (pc) {
//扫码支付
$pay = new nativePay;
}
return $pay;
}
}


//微信支付方法 省略了类
function wechatPay() {
//...省略
$pay = factory::createPay();
$pay->pay();
}


这样拆分有什么好处呢?

虽然增加支付的时候依旧需要修改代码,但是只需要修改简单工厂类,而不用修改业务逻辑类,避免了因为修改业务逻辑类而产生的业务逻辑bug。使得实例化对象和具体业务无关。

增加了代码的复用性,维护性,灵活性,测试性等。

代码放在了我的github上面。

设计模式之单例模式–php实现单例模式

单例模式是最常被提起的一个设计模式,他的意思是

一个类只有一个实例,所有人共用这同一个实例。不让外部创建类的实例,并且提供一个外部的全局访问点。

为什么要使用单例模式

单例模式有什么好处呢?

单例模式是为了全局唯一,如果你需要一个全局唯一的类那么就需要单例模式了。而且单例模式还可以节省资源,因为这个类只有一个对象。

比如你需要一个全局唯一的id生成器,如果创建了多个实例,那么生成的id可能就不是全局唯一了。

比如你需要一个全局配置信息,需要全局缓存信息,全局日志文件写入等等。

普通类

下面是一个普通的类的实现和使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Common {
public $name; //姓名
public $age; //年龄

function __construct($name, $age)
{
$this->name = $name;
$this->age = $age;
}

public function print() {
dump('姓名:'.$this->name);
dump('年龄:'.$this->age);
}
}

$obj1 = new Common('张三', '18');
dump($obj1);

$obj2 = new Common('张三', '20');
dump($obj1);

可以看到普通类可以new多个对象,并且每个对象都不一样,拥有自己的属性。

单例类

首先,单例类是一种特殊的类,单例类只允许new一次。怎么实现只能new一次呢,从上面的代码中可以看到,new这个操作是在我们的业务中进行的,这是个无法掌控的操作。

我们要让new操作在类中实现,由这个单例类来决定,来操作对象的实例化。而不是在业务中进行。

只有自己才能决定自己newnew,自己决定自己的人生,而不是交给别人,使得无法掌控。

从这个分析中可以知道,单例类要完成下面的操作:

  • 自己创建实例化,供外部全局访问
  • 不允许外部实例化

第一点简单,实现一个方法new自己,然后存起来返回就可以了。

第二点在php中怎么实现呢,不允许外部实例化可以通过将__construct方法设置为private权限。因为这个方法外部访问不到,所以外部就无法实例化了。

简单实现一下单例类。

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
class Singleton {
public $name; //姓名
public $age; //年龄
private static $instance;

private function __construct($name, $age)
{
$this->name = $name;
$this->age = $age;
}

public static function getInstance($name, $age) {
if (empty(self::$instance)) {
self::$instance = new self($name, $age);
}
return self::$instance;
}

public function print() {
dump('姓名:'.$this->name);
dump('年龄:'.$this->age);
}
}

$obj1 = Singleton::getInstance('张三', '18');
dump($obj1);
$obj2 = Singleton::getInstance('张三', '20');
dump($obj2);

这是把上面的普通类改成了单例类,现在这个单例类还有些问题。

  • 参数上面第二个参数没有生效

单例类一般不会出现这种需要各种参数的,需要参数也建议通过配置文件来搞定参数,而不是传参。如果有传参需求,需要的是不同的参数是不同的对象,同一个参数是单例对象,那么需要维护一个单例对象的数组。为什么不提供修改参数的方法呢,因为提供这个方法太危险,全局使用同一个对象,如果某个地方不慎更改了属性,那么会导致其他地方出现未知bug。

  • 并没有完全禁止外部实例化,php还有魔术方法__clone。

把__clone方法设置成private

根据上面的我们在修改一下这个单例类。

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

class Singleton {
protected $name; //姓名
protected $age; //年龄

//用来存放全局唯一对象
private static $instance;

/**
* 私有化构造函数使得外部无法new对象
* 构造函数去掉参数,参数从配置文件读取
*/
private function __construct()
{
$this->name = config('design.singleton.name');
$this->age = config('design.singleton.age');
}

/**
* 私有化克隆函数使得外部无法克隆
*/
private function __clone() {

}

/**
* 获取全局唯一对象的时候不需要传参
*/
public static function getInstance() {
if (empty(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}

public function print() {
dump('姓名:'.$this->name);
dump('年龄:'.$this->age);
}
}

$obj1 = Singleton::getInstance();
dump($obj1);
$obj2 = Singleton::getInstance();
dump($obj2);

到现在,我们就完成了一个基于php的单例类,这个单例类支持延迟加载。如果不需要延迟加载,那么实现更简单。看下面的饿汉式单例类

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

class Singleton {
protected $name; //姓名
protected $age; //年龄

//用来存放全局唯一对象 饿汉式单例直接在这里new
private static $instance = new self();

/**
* 私有化构造函数使得外部无法new对象
* 构造函数去掉参数,参数从配置文件读取
*/
private function __construct()
{
$this->name = config('design.singleton.name');
$this->age = config('design.singleton.age');
}

/**
* 私有化克隆函数使得外部无法克隆
*/
private function __clone() {

}

/**
* 获取全局唯一对象的时候不需要传参
* 这里只需要直接返回
*/
public static function getInstance() {
return self::$instance;
}

public function print() {
dump('姓名:'.$this->name);
dump('年龄:'.$this->age);
}
}

我们支持延迟加载的其实还有些问题,那就是缺少锁,如果很多请求同时进来,那么后面的会覆盖前面的单例对象。如果再加上锁的话实现是下面这样。

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

class Singleton {
protected $name; //姓名
protected $age; //年龄

//用来存放全局唯一对象
private static $instance;

/**
* 私有化构造函数使得外部无法new对象
* 构造函数去掉参数,参数从配置文件读取
*/
private function __construct()
{
$this->name = config('design.singleton.name');
$this->age = config('design.singleton.age');
}

/**
* 私有化克隆函数使得外部无法克隆
*/
private function __clone() {

}

/**
* 获取全局唯一对象的时候不需要传参
*/
public static function getInstance() {
//先加锁
$lock = new lock;
$lock->lock();
if (empty(self::$instance)) {
self::$instance = new self();
}
//释放锁
$lock->unlock();
return self::$instance;
}

public function print() {
dump('姓名:'.$this->name);
dump('年龄:'.$this->age);
}
}

这样的话因为锁的原因会造成性能比较低,所以又有了下面的方式,叫做双重检测。也就是拿锁之前先判断一下有没有实例化,如果有了就不需要锁了。为什么不直接在第一个判断里面加呢,因为锁加晚了没有意义了。

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

class Singleton {
protected $name; //姓名
protected $age; //年龄

//用来存放全局唯一对象
private static $instance;

/**
* 私有化构造函数使得外部无法new对象
* 构造函数去掉参数,参数从配置文件读取
*/
private function __construct()
{
$this->name = config('design.singleton.name');
$this->age = config('design.singleton.age');
}

/**
* 私有化克隆函数使得外部无法克隆
*/
private function __clone() {

}

/**
* 获取全局唯一对象的时候不需要传参
*/
public static function getInstance() {
//先判断
if (empty(self::$instance)) {
//先加锁
$lock = new lock;
$lock->lock();
if (empty(self::$instance)) {
self::$instance = new self();
}
//释放锁
$lock->unlock();
}

return self::$instance;
}

public function print() {
dump('姓名:'.$this->name);
dump('年龄:'.$this->age);
}
}

这就是整个单例模式。四种实现:

  • 饿汉式 在类加载的时候实例化
  • 懒汉式 支持延迟加载
  • 带锁懒汉式 延迟加载的时候加锁
  • 双重检测 两次判断

代码放在了我的github上面。