服务容器简介

服务容器是 Laravel 非常核心的内容,也可以说是 Laravel 中最引人注目的地方。提到服务容器,就不得不提到一大堆高大上的名词,依赖注入、控制反转、依赖倒置、反射等等。要了解 Laravel 是怎么实现服务容器的,也要先从这些名词入手,我们就一个一个地来看看。

依赖、依赖注入与控制反转

依赖,指的是一个类 A 的变化会引起另一个类 B 的变化,我们就说这个类 B 依赖于类 A 。还是用例子来说明会更清晰。

假设我们闲着没事了,想刷短视频,那么我们定义一个类,代表“我”,有个方法就叫“刷短视频”。

class ZyBlog{
public function ShuaDuanShiPin(){}
}

主体类有了,里面要写什么呢?我们要刷视频,那得有个手机对吧,于是我们再定义一个手机类,高大上一点,就是一个 iPhone12 吧。

class iPhone12{
public function openApp($name){
echo __CLASS__ . '打开' . $name;
}
}

好了,接下来我们让刷短视频的动作去打开 App ,也就是调用手机类的 openApp() 方法。

public function ShuaDuanShiPin(){
$iPhone = new iPhone12();
$iPhone->openApp('douyin');
}

功能完成了,高兴不,完美不,一切都是那么美好,但是,注意,这里但是来了就说明没啥好事了。我们想换一个手机,或者原来的手机坏了,这时候要怎么办呢?一个方法是直接修改 iPhone12 类,把它变成另一个手机,另一个方法是重新定义一个手机类,我们采用第二个方法。

<?php



class iPhone12{
public function openApp($name){
echo __CLASS__ . '打开' . $name;
}
}

class Mi11{
public function openApp($name){
echo __CLASS__ . '打开' . $name;
}
}

class ZyBlog{
public function ShuaDuanShiPin(){
$mi = new Mi11();
$mi->openApp('douyin');
}
}

我们又定义了一个小米手机,因为要换手机,所以我们的 iPhone12 要产生变化(类A变了),于是,我们的这个个体类(类B)也要跟着修改,这就是 依赖 。接下来,我们思考能不能让类A变化,类B尽量不动呢?这就需要先抽象类A,它们有一个相同的方法都是打开 App ,那么我们定义一个接口或者抽象类。在这里其实抽象类更合适,因为代码非常简单 openApp() 方法的内部实现是一样的。不过,更推荐的方式其实是定义一个接口,因为很有可能你的手机是老年功能机,或许根本没有 App 这个概念,那么老年机就不要去实现这个接口就好了。于是,我们就定义一个抽象的手机接口,然后让两个类去实现这个接口。

interface IntelligencePhone{
public function openApp($name);
}

class iPhone12 implements IntelligencePhone {
public function openApp($name){
echo __CLASS__ . '打开' . $name;
}
}

class Mi11 implements IntelligencePhone {
public function openApp($name){
echo __CLASS__ . '打开' . $name;
}
}

然后,我们秉承接口的特性,来实现刷视频的效果。

class ZyBlog{
public function ShuaDuanShiPin(IntelligencePhone $phone){
$phone->openApp('douyin');
}
}

(new ZyBlog())->ShuaDuanShiPin(new iPhone12()); // iPhone12打开douyin
(new ZyBlog())->ShuaDuanShiPin(new Mi11()); // Mi11打开douyin

看出来这个问题是如何解决的吗?没错,我们现在其实还有依赖,但是,我们的依赖是通过方法的参数注入过来的,这就叫做 依赖注入 。现在我们还需要修改个体类中刷短视频的实现吗?不需要了,我们将手机类对象的控制权交给了外部,这个外部就像是一个第三方,你的一个同学或者朋友,看见你拿什么手机在刷短视频,取决于他在 当下真实的看到 你手上拿的是什么手机。这样控制权转移到类实现的外部,就叫做 控制反转 。

从上面的例子其实可以出,依赖注入和控制反转其实是可以理解成一回事的,根据依赖的定义,如果想要让 类B 不再依赖 类A ,那么就得把实例化 类A 的控制权交到外部去,这其实就是一个控制反转的过程,而交到外部实例化之后的对象必须要想办法再交给 类B 去使用,这就是一个依赖注入的过程。

这些概念在技术实现层面上,其实就是利用的面向对象的封装、继承、多态、接口实现这些特性。目的嘛,当然就是了为那个 开放封闭原则 。要知道,代码在修改维护时是最容易出现问题的,而接口的契约性就能让你必须在规则范围之内去实现,因此,具体的调用方就可以放心地使用接口中的方法。

依赖注入的简称学名是 DI ,控制反转的简称学名是 IoC 。

服务容器 IoC Container

弄明白上面两个问题,那么 服务容器 这个概念其实也就很好理解了。从英文简写就可以看出,其实服务容器也就是一个控制返转容器。既然是容器,那么它其实就是保存我们所有需要要依赖的对象。

class Container
{
protected $binds;

protected $instances;

public function bind($abstract, $concrete)
{
if ($concrete instanceof Closure) {
$this->binds[$abstract] = $concrete;
} else {
$this->instances[$abstract] = $concrete;
}
}

public function make($abstract, $parameters = [])
{
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}

array_unshift($parameters, $this);

return call_user_func_array($this->binds[$abstract], $parameters);
}
}

这是网上非常经典的一段服务容器的代码实现,具体的原文链接还是在下方的参考链接处。从这段代码中,我们就可以看出容器无非就是两个数组,一个数组用于存储绑定的 回调函数 操作。另一个数组用于存储实例化之后的对象操作。

在 bind() 方法中,我们有两种对象接收方式。一种是使用回调函数,将这些回调函数放入到 binds 数组中。而另一种是直接的实例对象,实例对象放到 instances 数组中。当我们使用 make() 方法的时候,如果这个服务对象是一个具体的实例化对象,那么我们直接从 instances 中返回对应的对象就可以啦,如果我们之前绑定的是一个闭包函数,那么我们就去执行这个闭包函数。

为什么要使用闭包函数呢?其实闭包函数有一个非常大的好处就是可以 推后实例化 或者叫 延迟实例化。服务容器的核心思想是将所有依赖,也就是 new 实例化的工作统一管理起来。如果说我们全都是直接绑定实例化的对象,那么像 Laravel 这种级别的框架就可能在启动的时候就会占用大量的内存,因为我们需要将所有需要用到的对象都实例化出来。明显这是不现实的。而通过回调函数,我们就可以实现在需要的时候去实例化,而不用提前将大量的对象实例化好。

$zy = new ZyBlog();
dump($zy);

$callZy = function(){
return new ZyBlog();
};
// ……
// ……
// ……
$zy = $callZy();
dump($zy);

就像这段代码,我们完全可以在程序一开头就定义好各种回调函数,在这个时候回调函数没有调用过,里面的 new 并没有创建实例对象。然后在需要某个对象的时候,直接去调用这个回调函数来获得所需的对象。相信到这里你已经明白回调函数是个什么作用了吧。接下来,我们再细想一下,服务容器是在创建返回对象,而且还通过一个回调函数的方式延迟返回,好好回想一下,这不是一个工厂方法嘛!!

没错,如果要按设计模式来说的话,它真是属于一个创建型模式。要按亲属关系来看,它和 工厂方法 还真的非常亲近,另外,还有一个设计模式不知道你发现了没有。如果是 bind() 一个实例化对象的话,我们返回的会是一个全局唯一的对象,也就是一个 单例模式 或者是一个 享元模式 的实现,是不是非常非常像。OK,转化一下思路,一个工厂方法模式加上享元模式中的那个 factory 方法,是不是就成了一个服务容器了。看来之前的知识真的是没有白学啊。

我们先使用回调函数的方式来应用服务容器。

$container = new Container();

$container->bind(iPhone12::class, function(){
return new iPhone12();
});
$container->bind(Mi11::class, function(){
return new Mi11();
});
$container->bind(ZyBlog::class, function(){
return new ZyBlog();
});

$zyBlog1 = $container->make(ZyBlog::class);

$zyBlog1->ShuaDuanShiPin($container->make(iPhone12::class));

$zyBlog2 = $container->make(ZyBlog::class);

var_dump($zyBlog2 === $zyBlog1); // false

注意我们最后打印了两次 make() 出来的对象。由于每次调用回调函数都会创建一个新的对象,因此,make() 每次返回的都会是一个新的对象,它们肯定是不会全等的。接下来我们使用实例化的方式来加入到服务容器中。

$container2 = new Container();
$container2->bind(iPhone12::class, new iPhone12());
$container2->bind(Mi11::class, new Mi11());
$container2->bind(ZyBlog::class, new ZyBlog());

$zyBlog1 = $container2->make(ZyBlog::class);

$zyBlog1->ShuaDuanShiPin($container2->make(Mi11::class));

$zyBlog2 = $container2->make(ZyBlog::class);

var_dump($zyBlog2 === $zyBlog1); // true

很明显,在我们的服务容器中使用实例化的对象方式之后,对象会保存在 instances 中,也就是说它们都只会保存一份实例化对象,这时就是一种 单例 或者更像 享元 的形式,返回的是同一个对象了。

总结

最后我们总结一下,服务容器,真的就是个容器,这个容器里面放的是什么呢?就是我们需要的各种对象。它为了解决什么问题呢?解决的就是我们的依赖问题。够了,有这些相信你应对普通的面试问题不大了。需要背下来吗?这么简单一个容器类估计看两遍就记住了吧。另外我们还讲了一下回调函数来生产对象的好处以及与设计模式之间的相似性。

代码示例中的上半部分,也就是人和手机的部分是我自己写的,主要就是讲依赖注入这块临时想到的。个人感觉例子没有下面链接中大神的 超人 例子好,这个超人例子讲解服务容器非常经典,也是我很早就看到过的文章。强烈推荐大家放到收藏夹中不断回味学习。