Yii源依赖注入(容器)

有关概念

依赖倒置原则(Dependence Inversion Principle, DIP)

传统软件设计中,上层代码依赖于下层代码,当下层出现变动时,上层也要相应变化。

DIP的核心思想是:上层定义接口,下层实现这个接口,从而使的下层依赖于上层,降低耦合。

控制反转(Inversion of Control, IoC)

IoC是DIP的具体思路做法,IoC的核心是将类所依赖的下层单元的实例化过程交由第三方来实现。

一个简单的特征:类中不对所依赖的单元有诸如$component = new yii\component\SomeClass()的实例化语句。

依赖注入(Dependence Injection, DI)

DI是IoC的一种设计模式。

DI的核心是把类所依赖的单元的实例化过程,放到类的外面去实现。

控制反转容器(IoC Container)

当项目比较大时,依赖关系可能很复杂。而IoCC提供了动态地创建、注入依赖单元,映射依赖关系等功能。Yii设计了一个yii\di\Container来实现了DI Container。

服务定位器(Service Locator)

SL是IoC的另一种实现方式,其核心是把所有可能用到的依赖单元交给SL进行实例化和操作,把类对依赖单元的依赖,转换成类对SL的依赖。

Yii2通过DI容器,实现了SL。

依赖注入

DI在web中,常见于使用第三方服务实现特定功能(例:发邮件,推微博)。

假设要实现当访客在博客上发表评论后,向博文的作者发送Email的功能,通常代码如下:

// 为邮件服务定义抽象层
interface EmailSenderInterface{
    public function send();
}

// 定义Gmail服务
class GmailSender implements EmailSenderInterface{
    public function send()
}

// 定义评论类
class Comment extend yii\db\ActiveRecord{
    private $_eEmailSender;
    public function init(){
        $this->_eMailSender = GmailSender::getInstance();
    }

    public function afterInsert(){
        $this->_eMailSender->send();
    }
}

这个常见的设计方法有一个问题:Comment对于GmailSender的依赖,突然有一天不用Gmail了,那么必须修改init里的实例化语句。

同时,这个类的复用程度不高,下一个不用Gmail服务的项目,还需要再修改,或者直接去掉该邮件服务。

在Yii中使用DI解耦,有两种注入方式:构造函数注入、属性注入。

构造函数注入

class Comment extend yii\db\ActiveRecord{
    private $_eMailSender;
    public function __construct($emailSender){
        $this->_eMailSender = $emailSender;
    }

    public function afterInsert(){
        $this->_eMailSender->send();
    }
}

// 实例化两种不同的邮件服务,都继承了基类
$sender1 = new GmailSender();
$sender2 = new MyEmailSender();

$comment1 = new Comment($sender1);
$comment1.save();
$comment2 = new Comment($sender2);
$comment2.save();

属性注入

class Comment extend yii\db\ActiveRecord{
    private $_eMailSender;

    public function setEmailSender($value){
        $this->_eMailSender = $value;
    }

    public function afterInsert(){
        $this->_eMailSender->send();
    }
}

实际上,依赖注入就是从外面,将实例打到内部,从而完成整体的功能。

打入的方式有两种,一种是初始化是通过传参。另外一种是调用内部set方法,将实例注入属性,内部方法会调用该属性,进而完成功能。

DI容器

一个Web应用的某一组件会依赖于若干单元,这些单元又有可能依赖于基本单元,从而形成依赖嵌套的情形。

那么,这些依赖单元的实例化、注入过程的代码就会又长又繁杂,前后关系也需要注意。

yii\di\Container,通过DI容器,可以更好的管理对象及对象的所有依赖,以及这些依赖的依赖,进行实例化和配置。

DI容器中的内容

Yii使用yii\di\Instance来表示容器中的东西。Yii还将这个类用于Service Locator。

Instance本质上是DI容器中对于某一个类实例的引用,它的代码看起来并不复杂:

class Instance{
    // 保存类名,借口名,别名
    public $id;

    protected function __construct($id){}

    // 静态方法创建一个Instance实例
    public static function of($id){
        return new static($id);
    }

    // 将引用解析成实际的对象,并确保这个对象的类型
    public static function ensure($reference, $type = null, $container = null){}

    // 获取这个实例所引用的实际对象,事实上它调用的是yii\di\Container::get()
    public function get($container = null){}
}

该Instance:

  • 表示的是容器中的内容,代表的是对于实际对象的引用。

  • DI容器可以通过他获取所引用的实际对象。

  • 属性id表示实例的类型.

DI容器的数据结构

// 用于保存单例Singleton对象,以对象类型为键
private $_singletons = [];

// 用于保存依赖的定义,以对象类型为键
private $_definitions = [];

// 用于保存构造函数的参数,以对象类型为键
private $_params = [];

// 用于缓存ReflectionClass对象,以类名或接口名为键
private $_reflections = [];

// 用于缓存依赖信息,以类名或接口名为键
private $_dependencies = [];

注册依赖

使用DI容器,首先要告诉容器,类型及类型之间的依赖关系,声明这一关系的过程称为注册依赖

使用:yii\di\Container::set() & yii\di\Container::setSinglton()

在DI容器中,依赖关系的定义是唯一的。 后定义的同名依赖,会覆盖前面定义好的依赖。

对于 set() 而言,还要删除 $_singleton[] 中的同名依赖。 对于 setSingleton() 而言,则要将 $_singleton[] 中的同名依赖设为 null , 表示定义了一个Singleton,但是并未实现化。

$container = new \yii\di\Container;

// 直接以一个类名注册一个依赖
// $_definition['\yii\db\Connection'] = '\yii\db\Connection';
$container->set('\yii\db\Connection');

// 注册一个接口,当一个类依赖于该接口时,定义中的类会自动被实例化,并给有依赖需要的类使用
// $_definition['yii\mail\MailInterface'] = 'yii\swiftmailer\Mailer';
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

// 注册一个别名
$container->set('foo', 'yii\db\Connection');

// 用callable来注册一个别名,每次引用这个别名时,该callable都会被调用
$container->set('db', function($container, $params, $config){
    return new \yii\db\Connectin($config);
});

你可以这么理解:依赖的定义只是往特定的数据结构中写入有关的信息。

DI容器中装了两类实例,一种是单例,每次向容器索取单例类型的实例时,得到的都是同一个实例; 另一类是普通实例,每次向容器索要普通类型的实例时,容器会根据依赖信息创建一个新的实例给你。

对象的实例化

解析依赖信息

yii\di\Container::getDependencies()

该方法实质上就是通过PHP5的反射机制,通过类的构造函数的参数分析他所依赖的单元。然后统统缓存起来备用。

另一个与解析依赖信息相关的方法就是 yii\di\Container::resolveDependencies()

  • $_reflections以类(接口、别名)名为键, 缓存了这个类(接口、别名)的ReflcetionClass。一经缓存,便不会再更改。

  • $_dependencies以类(接口、别名)名为键,缓存了这个类(接口、别名)的依赖信息。

  • 这两个缓存数组都是在yii\di\Container::getDependencies()中完成。这个函数只是简单地向数组写入数据。

  • 经过yii\di\Container::resolveDependencies()处理,DI容器会将依赖信息转换成实例。 这个实例化的过程中,是向容器索要实例。也就是说,有可能会引起递归。

实例的创建

yii\di\Container::build()

DI容器只支持yii\base\Object类。也就是说,你只能向DI容器索要 yii\base\Object 及其子类。 再换句话说,如果你想你的类可以放在DI容器里,那么必须继承自 yii\base\Object 类。 但Yii中几乎开发者在开发过程中需要用到的类,都是继承自这个类。 一个例外就是上面提到的 yii\di\Instance 类。但这个类是供Yii框架自己使用的,开发者无需操作这个类。

递归获取依赖单元的依赖在于dependencies = $this->resolveDependencies($dependencies, $reflection)中。

getDependencies() 和 resolveDependencies() 为 build() 所用。 也就是说,只有在创建实例的过程中,DI容器才会去解析依赖信息、缓存依赖信息。

容器内容实例化过程

获取依赖实例化对象使用yii\di\Container::get()

在整个实例化过程中,一共有两个地方会产生递归:一是 get() , 二是 build() 中的 resolveDependencies() 。

实例

namespace app\models;

use yii\base\Object;
use yii\db\Connection;

interface UserFinderInterface{
    function findUser();
}

class UserFinder extends Object implements UserFinderInterface{
    public $db;

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

    pubic function findUser(){}
}

class UserLister extends Object{
    public $finder;

    // 依赖接口
    public function __construct(UserFinderInterface $finder, $config = []){
        $this->finder = $finder;
        parent::__construct($config);
    }
}

一般做法:

$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);

团队开发的时候,很多类需要制定为单例模式,否则N个模块有N个服务就。。

上部代码改成DI容器

use yii\di\Container;

$container = new Container;

$container->set('yii\db\Connection', [...]);

$container->set('app\models\UserFinderInterface', [
    'class' => 'app\models\UserFinder',
]);

$container->set('userLister', 'app\models\UserLister');

// 获取该别名class的实例
$lister = $container->get('userLister');

DI容器维护了两个缓存数组 $_reflections$_dependencies 。这两个数组只写入一次,就可以无限次使用。 因此,减少了对ReflectionClass的使用,提高了DI容器解析依赖和获取实例的效率。

但是,对于典型的Web应用而言, 有许多模块其实应当注册为单例的,比如上面的 yii\db\Connection。一个Web应用一般使用一个数据库连接,特殊情况下会用多几个,所以这些数据库连接一般是给定不同别名加以区分后, 分别以单例形式放在容器中的。因此,实际获取实例时,步骤会简单得。对于单例, 在第一次get()时,直接就返回了。而且,省去不重复构造实例的过程。

参考

  1. http://martinfowler.com/articles/injection.html

  2. http://www.digpage.com/di.html



转自 https://segmentfault.com/a/1190000004647331

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容