Yii源碼解讀-依賴注入(容器)

有關概念

依賴倒置原則(Dependence Inversion Principle, DIP)

傳統軟件設計中,上層代碼依賴於下層代碼,當下層出現變更時,上層也要相應變化。html

DIP的核心思想是:上層定義接口,下層實現這個接口,從而使的下層依賴於上層,下降耦合。web

控制反轉(Inversion of Control, IoC)

IoC是DIP的具體思路作法,IoC的核心是將類所依賴的下層單元的實例化過程交由第三方來實現。數據庫

一個簡單的特徵:類中不對所依賴的單元有諸如$component = new yii\component\SomeClass()的實例化語句。swift

依賴注入(Dependence Injection, DI)

DI是IoC的一種設計模式。設計模式

DI的核心是把類所依賴的單元的實例化過程,放到類的外面去實現。數組

控制反轉容器(IoC Container)

當項目比較大時,依賴關係可能很複雜。而IoCC提供了動態地建立、注入依賴單元,映射依賴關係等功能。Yii設計了一個yii\di\Container來實現了DI Container。緩存

服務定位器(Service Locator)

SL時IoC的另外一種實現方式,其核心是把全部可能用到的依賴單元交給SL進行實例化和操做,把類對依賴單元的依賴,轉換成類對SL的依賴。數據結構

Yii2經過DI容器,實現了SL。app

依賴注入

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

相關文章
相關標籤/搜索