yii2依赖注入DI与服务定位器SL详解

转自:http://www.digpage.com/di.html
DIP(依赖倒置原则,Dependence Inversion Principle)
传统软件设计中,上层代码依赖于下层代码,当下层出现变动时, 上层代码也要相应变化,维护成本较高。而DIP的核心思想是上层定义接口,下层实现这个接口, 从而使得下层依赖于上层,降低耦合度,提高整个系统的弹性。

IoC(控制反转,Inversion of Control)
IoC是一种实现DIP的方法。 IoC的核心是将类(上层)所依赖的单元(下层)的实例化过程交由第三方来实现。 一个简单的特征,就是类中不对所依赖的单元有诸如 $component = new yii\component\SomeClass() 的实例化语句。

IoC Container(控制反转容器)
当项目比较大时,依赖关系可能会很复杂。 而IoC Container提供了动态地创建、注入依赖单元,映射依赖关系等功能,减少了许多代码量。 Yii 设计了一个 yii\di\Container 来实现了 DI Container。



Ioc的2种实现方式 
DI(依赖注入,Dependence Injection)
DI是IoC的一种实现IoC的设计模式,DI的核心是把类所依赖的单元的实例化过程,放到类的外面去实现。

SL(服务定位器,Service Locator)
Service Locator是IoC的另一种实现方式, SL核心是把所有可能用到的依赖单元交由Service Locator进行实例化和创建、配置, 把类对依赖单元的依赖,转换成类对Service Locator的依赖。 DI 与 Service Locator并不冲突,两者可以结合使用。 目前,Yii2.0把这DI和Service Locator这两个东西结合起来使用,或者说通过DI容器,实现了Service Locator。


一、依赖注入
(1)目的:解耦,实现代码复用
(2)2种方式:构造函数注入和属性注入


二、DI容器--yii\di\Container
(1)DI容器的实例、DI容器的5种数据结构
$_definitions:用于保存依赖的定义,以对象类型为键
$_params:用于保存构造函数的参数,以对象类型为键
$_singletons:用于保存单例Singleton对象,以对象类型为键
$_reflections:用于缓存ReflectionClass对象,以类名或接口名为键
$_dependencies:用于缓存依赖信息,以类名或接口名为键
(2)DI容器的注册-解析-实例化-获取实例
<2.1>注册:yii\di\Container::set()------$_definitions、$_params、$_singletons

<2.2>解析:yii\di\Container::getDependencies()注册:yii\di\Container::resolveDependencies()-----$_reflections、$_dependencies
---getDependencies()方法实质上就是通过PHP5 的反射机制, 通过类的构造函数的参数分析他所依赖的单元。然后统统缓存起来备用。
DI容器通过构造函数解析其依赖的单元,它的设计目的就是:自动实例化对象及该对象所依赖的一切单元。
引出问题:为什么不通过解析setter注入函数的依赖?
因为要获取实例不一定需要为某属性注入外部依赖单元,但是却必须为其构造函数的参数准备依赖的外部单元。 当一个用于注入的属性必须在实例化时指定依赖单元,只是实例化而已,就意味着只需要调用构造函数。 至于setter注入可以在实例化后操作
---resolveDependencies()方法处理后,DI容器会将依赖信息转换成实例。 这个实例化的过程中,是向容器索要实例。也就是说,有可能会引起递归。
在整个实例化过程中,一共有两个地方会产生递归:一是get(),二是build()中的resolveDependencies() 

<2.3>实例化:yii\di\Container::build()

<2.4>获取实例:yii\di\Container::get()

(3)对UserLister的实例化分析、过程图http://www.digpage.com/_images/DI.png

三、SL服务定位器--yii\di\ServiceLocator
(1)2种数据结构:
$_components:用于缓存存Service Locator中的组件或服务的实例。 
$_definitions:用于保存这些组件或服务的定义。

(2)在Yii应用中使用Service Locator和DI容器
<2.1>DI容器的引入-----Yii::$container
我们知道,每个Yii应用都有一个入口脚本 index.php 。在其中,有一行不怎么显眼:

require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
这一行看着普通,也就是引入一个 Yii.php 的文件。但是,让我们来看看这个 Yii.php
require(__DIR__ . '/BaseYii.php');
class Yii extends \yii\BaseYii
{
}
spl_autoload_register(['Yii', 'autoload'], true, true);
Yii::$classMap = include(__DIR__ . '/classes.php');
// 重点看这里。创建一个DI 容器,并由 Yii::$container 引用
Yii::$container = new yii\di\Container;

<2.2>SL的引入-----Yii::$app
Application的本质
继承关系:
yii\web\Application » yii\base\Application » yii\base\Module » yii\di\ServiceLocator » yii\base\Component » yii\base\Object
看看入口脚本 index.php 的最后两行:

$application = new yii\web\Application($config);
$application->run();
创建了一个 yii\web\Application 实例,并调用其 run() 方法。 那么,这个 yii\web\Application 是何方神圣? 
首先, yii\web\Application 继承自 yii\base\Application ,
yii\base\Application 又继承自 yii\base\Module ,说明所有的Application都是Module,
yii\base\Module 又继承自 yii\di\ServiceLocator,所有的Module都是服务定位器Service Locator,
因此,所有的Application也都是Service Locator。
同时,在Application的构造函数中, yii\base\Application::__construct()
public function __construct($config = [])
{
    Yii::$app = $this;
    ... ...
}
第一行代码就把Application当前的实例,赋值给Yii::$app了。 这意味着Yii应用创建之后,可以随时通过 Yii::$app 来访问应用自身,也就是访问Service Locator。


(3)DI与SL的紧密联系-----BaseYii::createObject() 
Service Locator和DI容器的亲密关系就隐藏在 yii\di\ServiceLocator::get() 获取实例时, 调用的 Yii::createObject() 中。 前面我们说到这个 Yii 继承自 yii\BaseYii ,因此这个函数实际上是 BaseYii::createObject() , 其代码如下:
// static::$container就是上面说的引用了DI容器的静态变量
public static function createObject($type, array $params = [])
{
    // 字符串,代表一个类名、接口名、别名。
    if (is_string($type)) {
        return static::$container->get($type, $params);

    // 是个数组,代表配置数组,必须含有 class 元素。
    } elseif (is_array($type) && isset($type['class'])) {
        $class = $type['class'];
        unset($type['class']);
        // 调用DI容器的get() 来获取、创建实例
        return static::$container->get($class, $params, $type);
    // 是个PHP callable则调用其返回一个具体实例。
    } elseif (is_callable($type, true)) {
        // 是个PHP callable,那就调用它,并将其返回值作为服务或组件的实例返回
        return call_user_func($type, $params);
    // 是个数组但没有 class 元素,抛出异常
    } elseif (is_array($type)) {
        throw new InvalidConfigException(
        'Object configuration must be an array containing a "class" element.');
    // 其他情况,抛出异常
    } else {
        throw new InvalidConfigException(
            "Unsupported configuration type: " . gettype($type));
    }
}
解析:
<3.1>这个 createObject() 提供了一个向DI容器获取实例的接口, 对于不同的定义,除了PHP callable外, createObject() 都是调用了DI容器的 yii\di\Container::get() , 来获取实例的。 Yii::createObject() 就是Service Locator和DI容器亲密关系的证明, 也是Service Locator构建于DI容器之上的证明。而Yii中所有的Module, 包括Application都是Service Locator,因此,它们也都构建在DI容器之上。
<3.2>在Yii框架代码中,只要创建实例,就是调用 Yii::createObject() 这个方法来实现。 可以说,Yii中所有的实例(除了Application,DI容器自身等入口脚本中实例化的),都是通过DI容器来获取的。
<3.3>Yii 的基类 yii\BaseYii ,所有的成员变量和方法都是静态的, 其中的DI容器是个静态成员变量 $container。 因此,DI容器就形成了最常见形式的单例模式,在内存中仅有一份,所有的Service Locator (Module和Application)都共用这个DI容器。 就就节省了大量的内存空间和反复构造实例的时间。
<3.4>DI容器的单例化,使得Yii不同的模块共用组件成为可能。 可以想像,由于共用了DI容器,容器里面的内容也是共享的。因此,你可以在A模块中改变某个组件的状态,而B模块中可以了解到这一状态变化。 但是,如果不采用单例模式,而是每个模块(Module或Application)维护一个自己的DI容器, 要实现这一点难度会大得多。


总结DI容器、Service Locator是如何配合使用的:
Yii 类提供了一个静态的 $container 成员变量用于引用DI容器。 在入口脚本中,会创建一个DI容器,并赋值给这个 $container 。
Service Locator通过 Yii::createObject() 来获取实例, 而这个 Yii::createObject() 是调用了DI容器的 yii\di\Container::get() 来向 Yii::$container 索要实例的。 因此,Service Locator最终是通过DI容器来创建、获取实例的。
所有的Module,包括Application都继承自 yii\di\ServiceLocator ,都是Service Locator。 因此,DI容器和Service Locator就构成了整个Yii的基础。


四、Yii创建实例的全过程
问题:DI容器的使用是要先注册依赖,后获取实例的。 但Service Locator在注册服务、组件时,又没有向DI容器注册依赖。那在获取实例的时候, DI容器怎么解析依赖并创建实例呢?
答:在向DI容器索要一个没有注册过依赖的类型时, DI容器视为这个类型不依赖于任何类型可以直接创建, 或者这个类型的依赖信息容器本身可以通过Reflection API自动解析出来,不用提前注册。
追问:如果某个类型需要依赖,但是我又没有创建,他怎么能生成呢?
答:其实你写过,在配置文件里的conponents:
return [
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=yii2advanced',
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8',
        ],
        'cache' => [
            'class' => 'yii\caching\MemCache',
            'servers' => [
                [
                    'host' => 'cache1.digpage.com',
                    'port' => 11211,
                    'weight' => 60,
                ],
                [
                    'host' => 'cache2.digpage.com',
                    'port' => 11211,
                    'weight' => 40,
                ],
            ],
        ],
        ... ...
    ],
];
配置文件是产生作用到应用中的全过程如下:
index.php最后一句:(new yii\web\Application($config))->run();点进此类中,
(1)从yii\web\Application开始往上找__construct():
追到yii\web\Application::construct(),最后一句Component::__construct();追到Component的父类方法BaseObject::__construct,里面Yii::configure($this, $config);追到BaseYii::configure()。
(2)从yii\web\Application开始往上找__set():
追到Component::__set(),其中$setter = 'set'.$name; $this->$setter($value);
(3)从yii\web\Application开始往上找setComponents:
追到ServiceLocator::setComponents(),
foreach ($components as $id => $component) {
    $this->set($id, $component);
}


总结如下:
配置数组会被 Yii::configure($config) 所调用,然后会变成调用Application的 setComponents(), 而Application其实就是一个Service Locator。setComponents()方法又会遍历传入的配置数组, 然后使用使用 Service Locator 的set() 方法注册服务。
因而,每次在配置文件的 components 项写入配置信息, 最终都是在向Application这个 Service Locator注册服务。
其实这就相当于注册了
Yii::$app->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=db.digpage.com;dbname=digpage.com',
    'username' => 'www.digpage.com',
    'password' => 'www.digapge.com',
    'charset' => 'utf8',
]);

Yii::$app->set('cache', [
    'class' => 'yii\caching\MemCache',
    'servers' => [
        [
            'host' => 'cache1.digpage.com',
            'port' => 11211,
            'weight' => 60,
        ],
        [
            'host' => 'cache2.digpage.com',
            'port' => 11211,
            'weight' => 40,
        ],
    ],
]);

五、DI实例

用DI容器创建一个接口类实例

app\common\UserLister.php

namespace app\common;
use yii\base\BaseObject;
use yii\db\Connection;

interface UserFinderInterface
{
    function findUser();
}

class UserFinder extends BaseObject implements UserFinderInterface
{
    public $db;

    public function __construct(Connection $db, $config = [])
    {
        $this->db = $db;
        parent::__construct($config);
    }

    public function findUser()
    {
    }
}

class UserLister extends BaseObject
{
    public $finder;

    public function __construct(UserFinderInterface $finder, $config = [])
    {
        $this->finder = $finder;
        parent::__construct($config);
    }
}
app\commands\TestController.php -----测试函数,set的顺序可以变化,不影响结果,容器自动递归调用 

namespace app\commands;
use Yii;
use yii\console\Controller;
use yii\di\Container;
use app\common\Test;
use yii\base\Event;
use app\common\MyBehavior;
use yii\db\ActiveRecord;
class TestController extends Controller
{
    public function actionTest3()
    {
        $container = new Container();
        $container->set('yii\db\Connection', [
            'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8',
        ]);
        $container->set('app\common\UserFinderInterface', [
            'class' => 'app\common\UserFinder',
        ]);
        $container->set('userLister', 'app\common\UserLister');

        $lister = $container->get('userLister');

        var_dump($lister);
    }

}


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值