盒子
盒子
文章目录
  1. 写在前
  2. 服务
  3. 依赖
  4. 简单工厂模式,依赖转移
  5. 依赖注入
  6. 服务容器
    1. 使用魔术方法构建服务容器
    2. 使用反射构建服务容器
    3. 为什么我们需要服务容器呢?
  7. Laravel服务容器
  8. 总结

谈谈服务容器

写在前

使用Laravel有段时间了,也做了大大小小好几个系统。得益于其优雅的设计理念、大而全的工具集,开发系统起来非常迅速,代码结构也十分整洁。利用点时间,探讨一下内部实现,看看Laravel到底做了什么。

我们知道,Laravel的核心就是服务容器,也是本文想着重探讨的。一开始看到这个名词,很晦涩。不要紧,一步一步来,先看看什么是服务?什么又是容器?最后再来结合看服务容器?

服务

日常生活中,谈到服务,自然会联想到“银行办卡”、“外卖配送”、“洗车”等等这些都是服务,这里我们看看,服务的概念是建立在双方的基础上,我去银行办卡,银行服务于我。我和银行之间构成了关系。

举个例子,一辆汽车有很多的配件(引擎、轮子、沙发..),这些配件组合成汽车,汽车有各种配件提供服务,配件服务于汽车,汽车与配件构成了关系。

再来个栗子,我去北京玩,可以坐大巴、坐飞机,还可以坐火车。我选择交通工具,交通工具服务于我,我和交通工具构成了关系。

你发现了,我在每个例子后面都写了“构成关系”,这个关系到底是什么?这就是依赖。汽车依赖配件,我依赖交通工具。

依赖

开始写写代码吧,

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 A {
public function saySomething()
{
echo "hello ";
}
}

class B {
private $obj = null;
public function __construct()
{
$this->obj = new A();
}

public function saySomething()
{
$this->obj->saySomething();
echo "world";
}
}

// Client
$b = new B();
$b->saySomething();

// hello world

在上面的代码中,我们做了这样一件事,输出“hello world”。我们知道A的功能是输出”hello “,那么为了不更改A的代码,我们新增了一个B,然后依赖于A,在构造函数里面把A组合进来,然后在B输出“world”之前,先调用A的方法输出“hello ”。

是的,我们的代码完成了预期的想法,成功输出“hello world”。现在,我不想输出“hello world”了,而是输出“bye world”。怎么做呢?

和前面一样,为了不改变A类的功能,我们新增一个C,功能是输出“bye”,如下所示:

1
2
3
4
5
6
class C {
public function saySomething()
{
echo "bye ";
}
}

但是,我们碰到了一个困难,如果想要输出“bye world”,必须改掉 B 的依赖 A,让 B 依赖 C,也就是需要改这一行:

1
2
3
4
$this->obj = new B();

// Todo 改成依赖 C
// $this->obj = new C();

这时候,我们陷入了沉思,现在改一下,好像也没什么,但是如果改完了,又叫我改回去,或者改成别的什么D、E、F、G怎么办?况且,现在只是依赖一个,如果以后依赖多了,那不是要吐血…比如下面这种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class B {
private $obj1 = null;
private $obj2 = null;
private $obj3 = null;
public function __construct()
{
$this->obj1 = new X();
$this->obj2 = new Y();
$this->obj3 = new Z();
// ....
}

public function saySomething()
{
$this->obj1->saySomething();
$this->obj2->saySomething();
$this->obj3->saySomething();
echo "world";
}
}

// Client
$b = new B();
$b->saySomething();

还有一种情况,如果这个类是通过远程方法调用的,根本就改不了。
细思极恐啊,每天加班加点就为了改这个,和咸鱼有什么区别!

简单工厂模式,依赖转移

为了不改这个 B,我们想到了一个办法,这些依赖能不能放在一个统一的地方,我需要的时候就去这个地方拿,改也是改这里,根本不用动原来的代码,看看怎么写

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
class SayFactory {
private $obj1 = null;
private $obj2 = null;
private $obj3 = null;

public function __construct()
{
$this->obj1 = new X();
$this->obj2 = new Y();
$this->obj3 = new Z();
}

public function saySomething()
{
$this->obj1->saySomething();
$this->obj2->saySomething();
$this->obj3->saySomething();
}
}

class B {
private $obj = null;
public function __construct()
{
$this->obj = new SayFactory();
}

public function saySomething()
{
$this->obj->saySomething();
echo "world";
}
}

// Client
$b = new B();
$b->saySomething();

这下,终于不用改 B 了,我们创建了一个简单工厂,让这个工厂依赖于 B,把依赖转移到了 sayFactory 工厂上。以后,管你什么需求,我只需要改这个工厂类就行了!

等等,问题解决了吗?虽然我们不用改 B 了,但是,还是要改这个 sayFactory。一个功能业务改动或许很轻,但是,如果所有的功能业务都这样写,还不是和咸鱼无差?

上面我们说,现实生活中,到底是不是这样的,举个例子,有时候,我们身上只有工行的卡,但是附近只有农行的ATM机,这时候会发生什么?农行ATM机可以识别工行的卡!

回来继续看,如果按照刚刚的思路,建立一个简单工厂来组合依赖项,那么工行的卡接入农行的ATM机,农行ATM应该要改一下代码才可以接入啊,为什么ATM可以识别呢?

依赖注入

面向接口,而不是面向实现编程

这段话,是用来镇楼的,看不懂没关系,当你读完本文再回过头看自然会有所体会 :)

前面说这么多,总结一下就是

依赖不好维护,客户端动代码(改需求),内部实现也要改

注意这个排比句

能不能

  • 让客户端掌握控制权
  • 让客户端来决定加载的依赖
  • 让客户端和服务遵守相同的协议

一句一句来看,什么叫让客户端掌握控制权。原来啊,我们的控制权一直掌握在服务的手上,服务自己来控制依赖的目标,这些都是隐藏在服务背后的,看之前的例子,B 一直掌控着依赖的目标(A、X、Y、Z..),客户端改需求了,服务内部就要更改,这无疑是痛苦的。于是我们把控制权反转(IoC),让客户端来掌握控制权。

让客户端来决定加载的依赖,既然控制权反转到了客户端,客户端就可以自由的搭配依赖项,想加载什么依赖就加载什么,比如这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Z {
public function saySomething()
{
echo __CLASS__ . " hello ";
}
}

class B {
private $obj = null;
public function __construct(Z $obg)
{
$this->obj = $obg;
}

public function saySomething()
{
$this->obj->saySomething();
echo "world";
}
}

$b = new B(new Z());
$b->saySomething();

这样的想法很好,客户端决定加载依赖。可是,这样的代码还是存在问题,如果我想加载别的依赖(比如Y、Z…),那岂不是还是需要更改 B 的构造函数中的类名?

又想到工行卡可以适配农行ATM那个例子,现在我们明白了,因为这张卡遵守了银联的协议,这个协议就是我们说的让客户端和服务遵守相同的协议,因为这张卡实现的接口和ATM提供的接口是一致的,所以它们可以适配。

完整代码如下:

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
interface ISay {
public function saySomething();
}

class X implements ISay {
public function saySomething()
{
echo __CLASS__ . " hello ";
}
}

class Y implements ISay {
public function saySomething()
{
echo __CLASS__ . " hello ";
}
}

class Z implements ISay {
public function saySomething()
{
echo __CLASS__ . " hello ";
}
}

class B {
private $obj = null;
public function __construct(ISay $obj)
{
$this->obj = $obj;
}

public function saySomething()
{
$this->obj->saySomething();
echo "world";
}
}

// Client
$b = new B(new X());
$b->saySomething();

$b = new B(new Y());
$b->saySomething();

// X hello world
// Y hello world

我们终于解决了这个问题,通过接口,我们把依赖注入到服务中,实现控制反转。

依赖倒置原则(DIP)是软件设计的一种思想,控制反转(IoC)则是基于DIP衍生出的一种软件设计模式。依赖注入(DI)是IoC的具体实现方式之一,使用最为广泛。IoC容器是DI构造依赖注入的框架,它管理着依赖项的生命周期以及映射关系。

服务容器

前面我们把服务讲清楚了,接下来讲讲容器。容器在日常生活中也不难理解,泳池、杯子、地球、乃至人都可以说是容器,就是可以装东西的东西。在程序中,变量、对象、数组、队列、栈、堆都是容器。

使用魔术方法构建服务容器

先看看代码

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
72
73
74
interface ISay {
public function saySomething();
}

class X implements ISay {
public function saySomething()
{
echo __CLASS__ . " hello ";
}
}

class Y implements ISay {
public function saySomething()
{
echo __CLASS__ . " hello ";
}
}

class Z {
public function saySomething()
{
echo __CLASS__ . " hello ";
}
}

class B {
private $obj = null;
public function __construct(ISay $obj)
{
$this->obj = $obj;
}

public function saySomething()
{
$this->obj->saySomething();
echo "world";
}
}

class Container
{
private $register = array();

function __set($k, $c)
{
$this->register[$k] = $c;
}

function __get($k)
{
return $this->register[$k]($this);
}
}

// 服务容器
$c = new Container();

// 注册 X 服务到容器
$c->x = function () {
return new X();
};

// 注册 B 服务到容器
$c->b = function ($c) {
// B 服务依赖 X
return new B($c->x);
};


// 从容器中取得 B
$b = $c->b;
$b->saySomething();

// X hello world

这段代码前面没有什么不同,把一个个服务声明清楚。后面我们声明了一个容器(Containter),这个容器使用属性来保存服务,所以它是一个服务容器。我们使用了魔术方法,在给不可访问属性赋值时,set() 会被调用。读取不可访问属性的值时,get() 会被调用。

接下来,我们把所需的服务注册到容器中,属性名是服务名,属性值是一个闭包,这个闭包做真正的服务实例化工作。

使用反射构建服务容器

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
class Container
{
private $register = array();

public function __set($k, $c)
{
$this->register[$k] = $c;
}

public function __get($k)
{
return $this->build($this->register[$k]);
}

/**
* 自动绑定(Autowiring)自动解析(Automatic Resolution)
*
* @param string $className
* @return object
* @throws Exception
*/
public function build($className)
{
// 如果是匿名函数(Anonymous functions),也叫闭包函数(closures)
if ($className instanceof Closure) {
// 执行闭包函数,并将结果返回
return $className($this);
}

// 取得类的反射
$reflector = new ReflectionClass($className);

// 检查类是否可实例化, 排除抽象类abstract和对象接口interface
if (! $reflector->isInstantiable()) {
throw new Exception("Can't instantiate this.");
}

// 获取类的构造函数
$constructor = $reflector->getConstructor();

// 若无构造函数,直接实例化并返回
if (is_null($constructor)) {
return new $className;
}

// 取构造函数参数,通过 ReflectionParameter 数组返回参数列表
$parameters = $constructor->getParameters();

// 递归解析构造函数的参数
$dependencies = $this->getDependencies($parameters);

// 创建一个类的新实例,给出的参数将传递到类的构造函数。
return $reflector->newInstanceArgs($dependencies);
}

/**
* @param array $parameters
* @return array
* @throws Exception
*/
public function getDependencies($parameters)
{
$dependencies = [];

// 反射类的构造函数参数
foreach ($parameters as $parameter) {
/** @var ReflectionClass $dependency */
$dependency = $parameter->getClass();

if (is_null($dependency)) {
// 是变量,有默认值则设置默认值
$dependencies[] = $this->resolveNonClass($parameter);
} else {
// 是一个类,递归解析
$dependencies[] = $this->build($dependency->name);
}
}

return $dependencies;
}

/**
* @param ReflectionParameter $parameter
* @return mixed
* @throws Exception
*/
public function resolveNonClass($parameter)
{
// 有默认值则返回默认值
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}

throw new Exception('I have no idea what to do here.');
}
}

$c = new Container();

$c->x = 'X';
$c->b = function ($c) {
return new B($c->x);
};

// 从容器中取得 B
$foo = $c->b;
$foo->saySomething();

为什么我们需要服务容器呢?

在使用服务容器之前,我们在客户端需要对服务进行实例化并加载相应的依赖,对于客户端来说,做的事情太多了,既要实例化服务,又要处理依赖关系,这是耦合的。所以,我们把服务注册到服务容器中,让服务容器替我们做这些事情,我们不必要关心细节,容器负责实例化,注入依赖,处理依赖关系等工作,客户端只需要使用就好了。

Laravel服务容器

Laravel的核心就是服务容器,它的功能更多,我们前面写的使用反射来实现服务容器,可以说是Laravel服务容器的缩小版,实际上,我们只做了服务实现(build),服务解析的过程还有一个服务查找(make)我们没有做,我们的做法比较搓,直接就是服务容器的一个属性 $c->b 。

Laravel的服务容器源码在 Illuminate\Container 下,可以看到,它还实现了 ArrayAccess 接口,可以通过 $c[‘b’] 这种形式来访问服务。它还支持绑定一个 singeton (单例) ,虽然PHP是一次请求的生命周期,但是也会出现一次请求多次实例化单个对象的时候,这个时候,单例模式就很好,服务容器还能提供一个单例对象,避免多次解析。你也可以使用 instance 方法绑定一个已经存在的对象至容器中。后面的调用都会从容器中返回指定的实例。既然是容器,除了绑定类,对象,接口,当然还可以绑定数据,通过情景绑定注入需要的任何值。还可以注册容器事件,做一些监听的工作,比作监听某个类对象的解析,这时候,可以动态的设置额外属性到对象中,非常强大!

总结

  • 认识服务(组件或者依赖)的概念
  • 依赖注入,面向接口编程
  • 认识服务容器的作用与实现简单的服务容器
  • 学习Laravel服务容器的强大之处
请我喝一杯咖啡
扫一扫,支持funsoul
  • 微信扫一扫