本文的目标

  • 简单几步,轻松实现非侵入式缓存。

本文的涉及名词

  • 装饰模式 (也叫修饰模式,只是翻译不同)
  • 非侵入式
  • 依赖注入

名词解释

装饰模式

装饰模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,修饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。[修饰模式](https://zh.wikipedia.org/wiki/%E4%BF%AE%E9%A5%B0%E6%A8%A1%E5%BC%8F)

非侵入式

简单的说就是新加功能或者代码改原有代码

依赖注入

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。[控制反转](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)

具体实现

常见的缓存代码:

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

class UserModel extends BaseModel
{

public function getList($id)
{

$key = $this->getCacheKey($id);
$ret = Cache::get($key);
if (empty($ret)) {
$ret = $this->getFromDb($id);
Cache::put($key, $ret, 1000);
}
return $ret;
}

public function getCacheKey($id)
{

......
}
public function geFromDb($id)
{

......
}
}

这样的代码带来的问题是缓存系统和数据代码高度耦合,如果需要更换或者撤销缓存系统,代码很难修改,也不好预测风险。接下来我们通过简单的几步来实现非侵入式缓存系统。

第一步先抽象出接口

1
2
3
4
5
<?php 
interface UserModelInterface
{

public function getList($id);
}

第二步UserModel实现 UserModelInterface

1
2
3
4
5
6
7
8
9
<?php
class UserModel extends BaseModel implements UserModelInterface
{

public function getList($id)
{

return $this->getFromDb($id);
}
......
}

第三步使用装饰模式建立UserModelCache

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
<?php
class UserModelCache extends BaseModelCache implements UserModelInterface
{

public function __construct(UserModel $model)
{

parent::__construct($model);
}

public function getList($id)
{

return $this->cache(__METHOD__, $id, 10000);
}

/**
* 需要缓存的数据都可以通过
* 这个类里来实现,可以通过 UserModelInterface 约束都需要实现的方法
* 如果不需要缓存的方法,会通过 BaseModelCache 的 __call 方法拦截,
* 并且穿透到 UserModel 的类里面去调用。
*
* 一个不需要cache的方法调用过程:
* 例如调用 UserModelCache->getAll()方法,因为 UserModelCache 没有该方法,
* 所以会寻找父类 BaseModelCache ,父类
* 也没有该方法,会被 BaseModelCache 的 __call 拦截,最后走到 UserModel->getAll()
* 这样,只要在UserModel 实现这个 getAll 就可以了。
*
* 需要cache的方法调用:
* 调用 getList 会走到父类的 cache 方法,cache 方法根据传递参数生成了 cahce id, 通过
* Cache 获取数据,获取不到数据,还会走 UserModel -> getList 方法,这样就实现了缓存功能,
* 而在 UserModel 里面没有嵌入任何代码。
*
* 关于如果灵活实用 cahce 的问题,稍后会介绍。
*/

}

class BaseModelCache
{

protected $model = null;

public function __construct(BaseModel $model)
{

$this->model = $model;
}

public function __call($method, $params)
{

// 通过 __call 方法来穿透 UserModelCache 不需要缓存或者未实现的方法,让 __call 去调用
// UserModel 的方法
return call_user_func_array([$this->model, $method], $params);
}

protected function cache($method, $params, $expire)
{

$key = $this->getCacheKey($method, $params);
$ret = Cache::get($key);
if (!$ret || CACHE_OPEN !== true) {
$ret = call_user_func_array([$this->model, $method], $params);
Cache::put($key, $ret, $expire);
}
return $ret;
}

protected function getCacheKey($method, $params)
{

return $key ....
}
}

第四步灵活使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
// 如果没有使用框架提供的依赖注入机制,那么就要自己手动完成了。
// 例如调用 UserModel 的 getList 方法

class UserController
{

function index(UserModelInterface $user)
{
$user = new UserModelCache();

// 直接走 UserModel
// $user = new UserModel();
//
return $user->getList();
}

}

// 现在的很多框架都提供了依赖注入的功能,这些功能用起来更方便强大,可以让用户通过简单的配置
// 来实现注入的内容,下面用 lumen 和 yii2 做两个简单的说明

第五部 lumen 和 yii2 依赖注入

通过 lumen 的 ServiceProvider 来提供依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 
// 具体的路径和名称空间根据自己的实际情况来
//
$app->when('App\Http\Controllers\UserController')
->needs('App\Models\UserModelInterface')
->give('App\Models\Cache\UserModelCache');
//->give('App\Models\Cache\UserModel'); 不需要缓存

// 在 controller 里面直接就被注入了 UserModelCache
// 如果需要其他的缓存,只需要修改此处的 give 就行了,这样就实现了
// 灵活配置,不需要对底层的代码进行修改
//
class UserController
{

function index(UserModelInterface $user)
{

$user->getList();
}
}

yii2 是通过 ServiceLoader 和 DI 容器来实现的,涉及 Container 相关的类,具体可以参考 yii2 的文档

1
2
3
4
5
$container = new yii\di\Container();
$container->set('UserModelInterface', 'UserModelCache');

// 使用
$user = $container->get('UserModelInterface');

以上代都是为了演示,并不是真实使用环境,和真实环境有差异,请自行调整

END.