Bootstrap

初识依赖注入和Ioc容器

初始写法

当A类使用B类,最开始的写法是:在A类内部新建B类的对象,然后使用。
例如,现在有个控制器类,需要从Repository类来获取数据。原始写法:

<?php
class Repository{
    public function getData(){
        return "data";
    }
}

class Controller{
    private $repository;
    public function __construct(){
        $this->repository = new Repository();
    }

    public function showData(){
        $data = $this->repository->getData();
        echo $data;
    }
}

$c = new Controller();
$c->showData();

依赖注入

同样是A类使用B类:现在我们在外部创建B类的对象,然后传给A类。
现在A类不需要创建B类,只管调用B类的方法。
下面是从构造器注入的示例:

<?php
class Repository{
    public function getData(){ return "data"; }
}

class Controller{
    private $repository;
    public function __construct(Repository $repository){
        $this->repository = $repository;
    }

    public function showData(){
        $data = $this->repository->getData();
        echo $data;
    }
}

//在其他的某个地方,实例化并且注入
$repository = new Repository();
$c = new Controller($repository);
$c->showData();

依赖反转

借鉴这篇文档依赖反转准则是指

依赖应该是接口/约定或者抽象类,而不是具体的实现。

示例:

<?php
//首先定义一个获取数据的接口
interface RepositoryInterface{
    public function getData();
}

//一个具体的实现类,它从mysql数据库获取数据
class fromMysqlRepository implements RepositoryInterface{
    public function getData(){
        return 'this is data from mysql database';
    }
}

class Controller{
    private $repository;
    //注入的依赖是一个接口
    public function __construct(RepositoryInterface $repository){
        $this->repository = $repository;
    }

    public function showData(){
        $data = $this->repository->getData();
        echo $data;
    }
}

//实例化,注入
$repository = new fromMysqlRepository();
$c = new Controller($repository);
$c->showData();

某天,需要换一种获取数据的方式,比如从redis缓存中获取数据。这时候要修改的地方:

//新建一个类,也是实现RepositoryInterface接口,不过方法的具体实现是从redis获取数据。
class fromRedisRepository implements RepositoryInterface{
    public function getData(){
        return 'this is data from redis';
    }
}

$repository = new fromRedisRepository();//实例化的地方,只需要修改这处代码
$c = new Controller($repository);
$c->showData();

一个初级的Ioc容器

上面的例子,都是需要手动创建依赖、注入。 创建依赖和注入,可以交给Ioc容器去做。
IOC控制反转:即

“创建对象实例的控制权从代码控制剥离到IOC容器控制。”

一个示例(来源于这里:https://www.insp.top/article/learn-laravel-container。):

interface RepositoryInterface{
    public function getData();
}

class fromMysqlRepository implements RepositoryInterface{
    public function getData(){
        return 'this is data from mysql database';
    }
}

class fromRedisRepository implements RepositoryInterface{
    public function getData(){
        return 'this is data from redis';
    }
}

class Controller{
    private $repository;

    public function __construct(RepositoryInterface $repository){
        $this->repository = $repository;
    }

    public function showData(){
        $data = $this->repository->getData();
        echo $data;echo '<br>';
    }
}

//手动地创建依赖,并注入。现在要改变这个做法
//$repository = new fromMysqlRepository();
//$repository = new fromRedisRepository();
//$c = new Controller($repository);
//$c->showData();

//一个初级的容器
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);
    }
}

// 创建容器
$container = new Container;
// 向容器添加Controller的创建脚本
$container->bind('controller', function($container, $moduleName) {
    return new Controller($container->make($moduleName));
});
// 向容器添加fromMysqlRepository的创建脚本
$container->bind('mysql', function($container) {
    return new fromMysqlRepository;
});
// 同上
$container->bind('redis', function($container) {
    return new fromRedisRepository;
});

//在某处创建、调用。
$c1 = $container->make('controller',['mysql']);
$c1->showData();

解读

这个容器的例子来源于这里:https://www.insp.top/article/learn-laravel-container
我读到这个例子时,看了好一会才看懂。 已经看懂的请忽略这个“解读”
$container->make('controller',['mysql']);的时候,发生了什么事?
1、第一次进入容器的make方法:
array_unshift($parameters, $this);
重新组装了$parameters,有2个元素:第一个是容器,第二个是字符串’mysql’。

call_user_func_array($this->binds[$abstract], $parameters);
此时 $this->binds[$abstract] 是刚才绑定的一个闭包controller,也就是这样的内容:

function($container, $moduleName) {
    return new Controller($container->make($moduleName));
}

2、
controller这个闭包再次调用make。第二次进入容器的make方法:
array_unshift($parameters, $this);
此时传进来的$parameters是空数组,重新组装后,只有一个元素,也就是容器

call_user_func_array($this->binds[$abstract], $parameters);
此时 $this->binds[$abstract] 是另一个闭包 ‘mysql’,内容是

function($container) {
    return new fromMysqlRepository;
};

3、所以,$container->make('controller',['mysql']);这一句代码
做了2件事,先新建一个fromMysqlRepository对象;再新建一个controller对象,并且向它注入了上一步新建的fromMysqlRepository对象.

在Ioc容器中使用反射

laravel5.5 的容器位于vendor\laravel\framework\src\Illuminate\Container\Container.php
larave的Ioc容器中使用到了反射。 主要思路是通过反射获取对象的构造函数,进而获取构造函数参数,根据参数创建依赖,然后创建对象。
主要代码:

public function build($concrete){
    if ($concrete instanceof Closure) {   /* ...... */    }

    $reflector = new ReflectionClass($concrete);    // 获得反射的对象
    if (!$reflector->isInstantiable()) {   /* ...... */    }

    $this->buildStack[] = $concrete;// 获取构造器
    $constructor = $reflector->getConstructor();
    if (is_null($constructor)) {   /* ...... */    }

    $dependencies = $constructor->getParameters(); // 获取构造器参数(一组ReflectionParameter 对象)
    $instances = $this->resolveDependencies($dependencies); // 创建依赖

    array_pop($this->buildStack);
    return $reflector->newInstanceArgs($instances);// 利用依赖创建对象
}

protected function resolveDependencies(array $dependencies){
    $results = [];
    foreach ($dependencies as $dependency) {
        if ($this->hasParameterOverride($dependency)) {   /* ...依赖如果被重写过的处理... */        }
        $results[] = is_null($dependency->getClass())
            ? $this->resolvePrimitive($dependency)  //如果不是一个可实例化的类,就“bomb out with an error”
            : $this->resolveClass($dependency); //没问题,就用make()创建依赖的实例
    }
    return $results;
}

protected function resolveClass(ReflectionParameter $parameter){
    try {
        return $this->make($parameter->getClass()->name);
    } catch (BindingResolutionException $e) {
        /* ...处理... */
        throw $e;
    }
}
;