Yii源依賴注入(容器)

有關概念

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

傳統(tǒng)軟件設計中屯烦,上層代碼依賴于下層代碼狈癞,當下層出現(xiàn)變動時,上層也要相應變化嚷缭。

DIP的核心思想是:上層定義接口,下層實現(xiàn)這個接口耍贾,從而使的下層依賴于上層阅爽,降低耦合。

控制反轉(Inversion of Control, IoC)

IoC是DIP的具體思路做法逼争,IoC的核心是將類所依賴的下層單元的實例化過程交由第三方來實現(xiàn)优床。

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

依賴注入(Dependence Injection, DI)

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

DI的核心是把類所依賴的單元的實例化過程胆敞,放到類的外面去實現(xiàn)着帽。

控制反轉容器(IoC Container)

當項目比較大時,依賴關系可能很復雜移层。而IoCC提供了動態(tài)地創(chuàng)建仍翰、注入依賴單元,映射依賴關系等功能观话。Yii設計了一個yii\di\Container來實現(xiàn)了DI Container予借。

服務定位器(Service Locator)

SL是IoC的另一種實現(xiàn)方式,其核心是把所有可能用到的依賴單元交給SL進行實例化和操作频蛔,把類對依賴單元的依賴灵迫,轉換成類對SL的依賴。

Yii2通過DI容器晦溪,實現(xiàn)了SL瀑粥。

依賴注入

DI在web中,常見于使用第三方服務實現(xiàn)特定功能(例:發(fā)郵件三圆,推微博)狞换。

假設要實現(xiàn)當訪客在博客上發(fā)表評論后,向博文的作者發(fā)送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){}

    // 靜態(tài)方法創(chuàng)建一個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蹄溉,但是并未實現(xiàn)化。

$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容器中裝了兩類實例囱晴,一種是單例膏蚓,每次向容器索取單例類型的實例時,得到的都是同一個實例畸写; 另一類是普通實例驮瞧,每次向容器索要普通類型的實例時,容器會根據依賴信息創(chuàng)建一個新的實例給你枯芬。

對象的實例化

解析依賴信息

yii\di\Container::getDependencies()

該方法實質上就是通過PHP5的反射機制论笔,通過類的構造函數的參數分析他所依賴的單元。然后統(tǒng)統(tǒng)緩存起來備用千所。

另一個與解析依賴信息相關的方法就是 yii\di\Container::resolveDependencies() 狂魔。

  • $_reflections以類(接口、別名)名為鍵淫痰, 緩存了這個類(接口最楷、別名)的ReflcetionClass。一經緩存待错,便不會再更改籽孙。

  • $_dependencies以類(接口、別名)名為鍵火俄,緩存了這個類(接口犯建、別名)的依賴信息。

  • 這兩個緩存數組都是在yii\di\Container::getDependencies()中完成瓜客。這個函數只是簡單地向數組寫入數據胎挎。

  • 經過yii\di\Container::resolveDependencies()處理,DI容器會將依賴信息轉換成實例忆家。 這個實例化的過程中犹菇,是向容器索要實例。也就是說芽卿,有可能會引起遞歸揭芍。

實例的創(chuàng)建

yii\di\Container::build()

DI容器只支持yii\base\Object類。也就是說卸例,你只能向DI容器索要 yii\base\Object 及其子類称杨。 再換句話說肌毅,如果你想你的類可以放在DI容器里,那么必須繼承自 yii\base\Object 類姑原。 但Yii中幾乎開發(fā)者在開發(fā)過程中需要用到的類悬而,都是繼承自這個類。 一個例外就是上面提到的 yii\di\Instance 類锭汛。但這個類是供Yii框架自己使用的笨奠,開發(fā)者無需操作這個類。

遞歸獲取依賴單元的依賴在于dependencies = $this->resolveDependencies($dependencies, $reflection)中唤殴。

getDependencies() 和 resolveDependencies() 為 build() 所用般婆。 也就是說,只有在創(chuàng)建實例的過程中朵逝,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);

團隊開發(fā)的時候宇整,很多類需要制定為單例模式,否則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應用一般使用一個數據庫連接威鹿,特殊情況下會用多幾個,所以這些數據庫連接一般是給定不同別名加以區(qū)分后轨香, 分別以單例形式放在容器中的忽你。因此,實際獲取實例時臂容,步驟會簡單得科雳。對于單例根蟹, 在第一次get()時,直接就返回了糟秘。而且简逮,省去不重復構造實例的過程。

參考

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

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



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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末尿赚,一起剝皮案震驚了整個濱河市散庶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吼畏,老刑警劉巖督赤,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異泻蚊,居然都是意外死亡躲舌,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門性雄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來没卸,“玉大人,你說我怎么就攤上這事秒旋≡技疲” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵迁筛,是天一觀的道長煤蚌。 經常有香客問我,道長细卧,這世上最難降的妖魔是什么尉桩? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮贪庙,結果婚禮上蜘犁,老公的妹妹穿的比我還像新娘。我一直安慰自己止邮,他們只是感情好这橙,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著导披,像睡著了一般屈扎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撩匕,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天助隧,我揣著相機與錄音,去河邊找鬼。 笑死并村,一個胖子當著我的面吹牛巍实,可吹牛的內容都是我干的。 我是一名探鬼主播哩牍,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼棚潦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了膝昆?” 一聲冷哼從身側響起丸边,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎荚孵,沒想到半個月后妹窖,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡收叶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年骄呼,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片判没。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡蜓萄,死狀恐怖,靈堂內的尸體忽然破棺而出澄峰,到底是詐尸還是另有隱情嫉沽,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布俏竞,位于F島的核電站绸硕,受9級特大地震影響,放射性物質發(fā)生泄漏魂毁。R本人自食惡果不足惜玻佩,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望漱牵。 院中可真熱鬧夺蛇,春花似錦疚漆、人聲如沸酣胀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闻镶。三九已至,卻和暖如春丸升,著一層夾襖步出監(jiān)牢的瞬間铆农,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留墩剖,地道東北人猴凹。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像岭皂,于是被迫代替她去往敵國和親郊霎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內容