爲了下降代碼耦合程度,提升項目的可維護性,Yii採用多許多當下最流行又相對成熟的設計模式,包括了依賴注入(Denpdency Injection, DI)和服務定位器(Service Locator)兩種模式。 關於依賴注入與服務定位器, Inversion of Control Containers and the Dependency Injection pattern 給出了很詳細的講解,這裏結合Web應用和Yii具體實現進行探討,以加深印象和理解。 這些設計模式對於提升自身的設計水平頗有幫助,這也是咱們學習Yii的一個重要出發點。php
在瞭解Service Locator 和 Dependency Injection 以前,有必要先來了解一些高大上的概念。 別擔憂,你只須要有個大體瞭解就OK了,若是展開來講,這些東西能夠單獨寫個研究報告:html
是否是雲裏霧裏的?沒錯,所謂「高大上」的玩意每每就是這樣,看着很炫,很唬人。 賣護膚品的難道會跟你說其實皮膚表層是角質層,不具吸取功能麼?這玩意又不考試,大體意會下就OK了。 萬一哪天要在妹子面前要裝一把範兒的時候,張口也能來這麼幾個「高大上」就好了。 但具體的內涵,咱們仍是要要經過下面的學習來加深理解,畢竟要把「高大上」的東西用好,發揮出做用來。mysql
首先講講DI。在Web應用中,很常見的是使用各類第三方Web Service實現特定的功能,好比發送郵件、推送微博等。 假設要實現當訪客在博客上發表評論後,向博文的做者發送Email的功能,一般代碼會是這樣:sql
// 爲郵件服務定義抽象層 interface EmailSenderInterface { public function send(...); } // 定義Gmail郵件服務 class GmailSender implements EmailSenderInterface { ... // 實現發送郵件的類方法 public function send(...) { ... } } // 定義評論類 class Comment extend yii\db\ActiveRecord { // 用於引用發送郵件的庫 private $_eMailSender; // 初始化時,實例化 $_eMailSender public function init() { ... // 這裏假設使用Gmail的郵件服務 $this->_eMailSender = GmailSender::getInstance(); ... } // 當有新的評價,即 save() 方法被調用以後中,會觸發如下方法 public function afterInsert() { ... // $this->_eMailSender->send(...); ... } }
上面的代碼只是一個示意,大體是這麼個流程。數據庫
那麼這種常見的設計方法有什麼問題呢? 主要問題在於 Comment 對於 GmailSender 的依賴(對於EmailSenderInterface的依賴不可避免), 假設有一天忽然不使用Gmail提供的服務了,改用Yahoo或自建的郵件服務了。 那麼,你不得不修改 Comment::init() 裏面對 $_eMailSender 的實例化語句:swift
$this->_eMailSender = MyEmailSender::getInstance();
這個問題的本質在於,你今天寫完這個Comment,只能用於這個項目,哪天你開發別的項目要實現相似的功能, 你還要針對新項目使用的郵件服務修改這個Comment。代碼的複用性不高呀。 有什麼辦法能夠不改變Comment的代碼,就能擴展成對各類郵件服務都支持麼? 換句話說,有辦法將Comment和GmailSender解耦麼?有辦法提升Comment的普適性、複用性麼?設計模式
依賴注入就是爲了解決這個問題而生的,固然,DI也不是惟一解決問題的辦法,畢竟條條大路通羅馬。 Service Locator也是能夠實現解耦的。數組
在Yii中使用DI解耦,有2種注入方式:構造函數注入、屬性注入。緩存
構造函數注入經過構造函數的形參,爲類內部的抽象單元提供實例化。 具體的構造函數調用代碼,由外部代碼決定。具體例子以下:數據結構
// 這是構造函數注入的例子 class Comment extend yii\db\ActiveRecord { // 用於引用發送郵件的庫 private $_eMailSender; // 構造函數注入 public function __construct($emailSender) { ... $this->_eMailSender = $emailSender; ... } // 當有新的評價,即 save() 方法被調用以後中,會觸發如下方法 public function afterInsert() { ... // $this->_eMailSender->send(...); ... } } // 實例化兩種不一樣的郵件服務,固然,他們都實現了EmailSenderInterface sender1 = new GmailSender(); sender2 = new MyEmailSender(); // 用構造函數將GmailSender注入 $comment1 = new Comment(sender1); // 使用Gmail發送郵件 $comment1.save(); // 用構造函數將MyEmailSender注入 $comment2 = new Comment(sender2); // 使用MyEmailSender發送郵件 $comment2.save();
上面的代碼對比原來的代碼,解決了Comment類對於GmailSender等具體類的依賴,經過構造函數,將相應的實現了 EmailSenderInterface接口的類實例傳入Comment類中,使得Comment類能夠適用於不一樣的郵件服務。 今後之後,不管要使用何何種郵件服務,只需寫出新的EmailSenderInterface實現便可, Comment類的代碼再也不須要做任何更改,多爽的一件事,擴展起來、測試起來都省心省力。
與構造函數注入相似,屬性注入經過setter或public成員變量,將所依賴的單元注入到類內部。 具體的屬性寫入,由外部代碼決定。具體例子以下:
// 這是屬性注入的例子 class Comment extend yii\db\ActiveRecord { // 用於引用發送郵件的庫 private $_eMailSender; // 定義了一個 setter() public function setEmailSender($value) { $this->_eMailSender = $value; } // 當有新的評價,即 save() 方法被調用以後中,會觸發如下方法 public function afterInsert() { ... // $this->_eMailSender->send(...); ... } } // 實例化兩種不一樣的郵件服務,固然,他們都實現了EmailSenderInterface sender1 = new GmailSender(); sender2 = new MyEmailSender(); $comment1 = new Comment; // 使用屬性注入 $comment1->eMailSender = sender1; // 使用Gmail發送郵件 $comment1.save(); $comment2 = new Comment; // 使用屬性注入 $comment2->eMailSender = sender2; // 使用MyEmailSender發送郵件 $comment2.save();
上面的Comment若是將 private $_eMailSender 改爲 public $eMailSender 並刪除 setter函數, 也是能夠達到一樣的效果的。
與構造函數注入相似,屬性注入也是將Comment類所依賴的EmailSenderInterface的實例化過程放在Comment類之外。 這就是依賴注入的本質所在。爲何稱爲注入?從外面把東西打進去,就是注入。什麼是外,什麼是內? 要解除依賴的類內部就是內,實例化所依賴單元的地方就是外。
從上面DI兩種注入方式來看,依賴單元的實例化代碼是一個重複、繁瑣的過程。 能夠想像,一個Web應用的某一組件會依賴於若干單元,這些單元又有可能依賴於更低層級的單元, 從而造成依賴嵌套的情形。那麼,這些依賴單元的實例化、注入過程的代碼可能會比較長,先後關係也須要特別地注意, 必須將被依賴的放在須要注入依賴的前面進行實例化。 這實在是一件既沒技術含量,又吃力不出成果的工做,這類工做是高智商(懶)人羣的天敵, 咱們是不會去作這麼無聊的事情的。
就像極其不想洗衣服的人發明了洗衣機(我臆想的,未考證)同樣,爲了解決這一無聊的問題,DI容器被設計出來了。 Yii的DI容器是 yii\di\Container ,這個容器繼承了發明人的高智商, 他知道如何對對象及對象的全部依賴,和這些依賴的依賴,進行實例化和配置。
容器顧名思義是用來裝東西的,DI容器裏面的東西是什麼呢?Yii使用 yii\di\Instance 來表示容器中的東西。 固然Yii中還將這個類用於Service Locator,這個在講Service Locator時再具體談談。
yii\di\Instance 本質上是DI容器中對於某一個類實例的引用,它的代碼看起來並不複雜:
class Instance { // 僅有的屬性,用於保存類名、接口名或者別名 public $id; // 構造函數,僅將傳入的ID賦值給 $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) { } }
對於 yii\di\Instance ,咱們要了解:
在DI容器中,維護了5個數組,這是DI容器功能實現的基礎:
// 用於保存單例Singleton對象,以對象類型爲鍵 private $_singletons = []; // 用於保存依賴的定義,以對象類型爲鍵 private $_definitions = []; // 用於保存構造函數的參數,以對象類型爲鍵 private $_params = []; // 用於緩存ReflectionClass對象,以類名或接口名爲鍵 private $_reflections = []; // 用於緩存依賴信息,以類名或接口名爲鍵 private $_dependencies = [];
DI容器的5個數組內容和做用如 DI容器5個數組示意圖 所示。
使用DI容器,首先要告訴容器,類型及類型之間的依賴關係,聲明一這關係的過程稱爲註冊依賴。 使用 yii\di\Container::set() 和 yii\di\Container::setSinglton() 能夠註冊依賴。
DI容器是怎麼管理依賴的呢?要先看看 yii\di\Container::set() 和 yii\Container::setSinglton()
public function set($class, $definition = [], array $params = []) { // 規範化 $definition 並寫入 $_definitions[$class] $this->_definitions[$class] = $this->normalizeDefinition($class, $definition); // 將構造函數參數寫入 $_params[$class] $this->_params[$class] = $params; // 刪除$_singletons[$class] unset($this->_singletons[$class]); return $this; } public function setSingleton($class, $definition = [], array $params = []) { // 規範化 $definition 並寫入 $_definitions[$class] $this->_definitions[$class] = $this->normalizeDefinition($class, $definition); // 將構造函數參數寫入 $_params[$class] $this->_params[$class] = $params; // 將$_singleton[$class]置爲null,表示還未實例化 $this->_singletons[$class] = null; return $this; }
這兩個函數功能相似沒有太大區別,只是 set() 用於在每次請求時構造新的實例返回, 而 setSingleton() 只維護一個單例,每次請求時都返回同一對象。
表如今數據結構上,就是 set() 在註冊依賴時,會把使用 setSingleton() 註冊的依賴刪除。 不然,在解析依賴時,你讓Yii到底是依賴續絃仍是原配?所以,在DI容器中,依賴關係的定義是惟一的。 後定義的同名依賴,會覆蓋前面定義好的依賴。
從形參來看,這兩個函數的 $class 參數接受一個類名、接口名或一個別名,做爲依賴的名稱。 $definition 表示依賴的定義,能夠是一個類名、配置數組或一個PHP callable。
這兩個函數,本質上只是將依賴的有關信息寫入到容器的相應數組中去。 在 set() 和 setSingleton() 中,首先調用 yii\di\Container::normalizeDefinition() 對依賴的定義進行規範化處理,其代碼以下:
protected function normalizeDefinition($class, $definition) { // $definition 是空的轉換成 ['class' => $class] 形式 if (empty($definition)) { return ['class' => $class]; // $definition 是字符串,轉換成 ['class' => $definition] 形式 } elseif (is_string($definition)) { return ['class' => $definition]; // $definition 是PHP callable 或對象,則直接將其做爲依賴的定義 } elseif (is_callable($definition, true) || is_object($definition)) { return $definition; // $definition 是數組則確保該數組定義了 class 元素 } elseif (is_array($definition)) { if (!isset($definition['class'])) { if (strpos($class, '\\') !== false) { $definition['class'] = $class; } else { throw new InvalidConfigException( "A class definition requires a \"class\" member."); } } return $definition; // 這也不是,那也不是,那就拋出異常算了 } else { throw new InvalidConfigException( "Unsupported definition type for \"$class\": " . gettype($definition)); } }
規範化處理的流程以下:
總之,對於 $_definitions 數組中的元素,它要麼是一個包含了」class」 元素的數組,要麼是一個PHP callable, 再要麼就是一個具體對象。這就是規範化後的最終結果。
在調用 normalizeDefinition() 對依賴的定義進行規範化處理後, set() 和 setSingleton() 以傳入的 $class 爲鍵,將定義保存進 $_definition[] 中, 將傳入的 $param 保存進 $_params[] 中。
對於 set() 而言,還要刪除 $_singleton[] 中的同名依賴。 對於 setSingleton() 而言,則要將 $_singleton[] 中的同名依賴設爲 null , 表示定義了一個Singleton,可是並未實現化。
這麼講可能很差理解,舉幾個具體的依賴定義及相應數組的內容變化爲例,以加深理解:
$container = new \yii\di\Container; // 直接以類名註冊一個依賴,雖然這麼作沒什麼意義。 // $_definition['yii\db\Connection'] = 'yii\db\Connetcion' $container->set('yii\db\Connection'); // 註冊一個接口,當一個類依賴於該接口時,定義中的類會自動被實例化,並供 // 有依賴須要的類使用。 // $_definition['yii\mail\MailInterface', 'yii\swiftmailer\Mailer'] $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer'); // 註冊一個別名,當調用$container->get('foo')時,能夠獲得一個 // yii\db\Connection 實例。 // $_definition['foo', 'yii\db\Connection'] $container->set('foo', 'yii\db\Connection'); // 用一個配置數組來註冊一個類,須要這個類的實例時,這個配置數組會發生做用。 // $_definition['yii\db\Connection'] = [...] $container->set('yii\db\Connection', [ 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ]); // 用一個配置數組來註冊一個別名,因爲別名的類型不詳,所以配置數組中須要 // 有 class 元素。 // $_definition['db'] = [...] $container->set('db', [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=127.0.0.1;dbname=demo', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ]); // 用一個PHP callable來註冊一個別名,每次引用這個別名時,這個callable都會被調用。 // $_definition['db'] = function(...){...} $container->set('db', function ($container, $params, $config) { return new \yii\db\Connection($config); }); // 用一個對象來註冊一個別名,每次引用這個別名時,這個對象都會被引用。 // $_definition['pageCache'] = anInstanceOfFileCache $container->set('pageCache', new FileCache);
setSingleton() 對於 $_definition 和 $_params 數組產生的影響與 set() 是同樣同樣的。 不一樣之處在於,使用 set() 會unset $_singltons 中的對應元素,Yii認爲既然你都調用 set() 了,說明你但願這個依賴再也不是單例了。 而 setSingleton() 相比較於 set() ,會額外地將 $_singletons[$class] 置爲 null 。 以此來表示這個依賴已經定義了一個單例,可是還沒有實例化。
從 set() 和 setSingleton() 來看, 可能還不容易理解DI容器,好比咱們說DI容器中維護了5個數組,可是依賴註冊過程只涉及到其中3個。 剩下的 $_reflections 和 $_dependencies 是在解析依賴的過程當中完成構建的。
從DI容器的5個數組來看也好,從容器定義了 set() 和 setSingleton() 兩個定義依賴的方法來看也好, 不難猜出DI容器中裝了兩類實例,一種是單例,每次向容器索取單例類型的實例時,獲得的都是同一個實例; 另外一類是普通實例,每次向容器索要普通類型的實例時,容器會根據依賴信息建立一個新的實例給你。
單例類型主要用於節省構建實例的時間、節省保存實例的內存、共享數據等。而普通類型主要用於避免數據衝突。
對象的實例化過程要比依賴的定義過程複雜得多。畢竟依賴的定義只是往特定的數據結構 $_singletons $_definitions 和 $_params 3個數組寫入有關的信息。 稍複雜的東西也就是定義的規範化處理了。其它真沒什麼複雜的。像你這麼聰明的,確定以爲這太沒挑戰了。
而對象的實例化過程要相對複雜,這一過程會涉及到複雜依賴關係的解析、涉及依賴單元的實例化等過程。 且讓咱們抽絲剝繭地進行分析。
容器在獲取實例以前,必須解析依賴信息。 這一過程會涉及到DI容器中還沒有提到的另外2個數組 $_reflections 和 $_dependencies 。 yii\di\Container::getDependencies() 會向這2個數組寫入信息,而這個函數又會在建立實例時,由 yii\di\Container::build() 所調用。 如它的名字所示意的, yii\di\Container::getDependencies() 方法用於獲取依賴信息,讓咱們先來看看這個函數的代碼
protected function getDependencies($class) { // 若是已經緩存了其依賴信息,直接返回緩存中的依賴信息 if (isset($this->_reflections[$class])) { return [$this->_reflections[$class], $this->_dependencies[$class]]; } $dependencies = []; // 使用PHP5 的反射機制來獲取類的有關信息,主要就是爲了獲取依賴信息 $reflection = new ReflectionClass($class); // 經過類的構建函數的參數來了解這個類依賴於哪些單元 $constructor = $reflection->getConstructor(); if ($constructor !== null) { foreach ($constructor->getParameters() as $param) { if ($param->isDefaultValueAvailable()) { // 構造函數若是有默認值,將默認值做爲依賴。即然是默認值了, // 就確定是簡單類型了。 $dependencies[] = $param->getDefaultValue(); } else { $c = $param->getClass(); // 構造函數沒有默認值,則爲其建立一個引用。 // 就是前面提到的 Instance 類型。 $dependencies[] = Instance::of($c === null ? null : $c->getName()); } } } // 將 ReflectionClass 對象緩存起來 $this->_reflections[$class] = $reflection; // 將依賴信息緩存起來 $this->_dependencies[$class] = $dependencies; return [$reflection, $dependencies]; }
前面講了 $_reflections 數組用於緩存 ReflectionClass 實例,$_dependencies 數組用於緩存依賴信息。 這個 yii\di\Container::getDependencies() 方法實質上就是經過PHP5 的反射機制, 經過類的構造函數的參數分析他所依賴的單元。而後通通緩存起來備用。
爲何是經過構造函數來分析其依賴的單元呢? 由於這個DI容器設計出來的目的就是爲了實例化對象及該對象所依賴的一切單元。 也就是說,DI容器必然構造類的實例,必然調用構造函數,那麼必然爲構造函數準備並傳入相應的依賴單元。 這也是咱們開頭講到的構造函數依賴注入的後續延伸應用。
可能有的讀者會問,那不是還有setter注入麼,爲何不用解析setter注入函數的依賴呢? 這是由於要獲取實例不必定須要爲某屬性注入外部依賴單元,可是卻必須爲其構造函數的參數準備依賴的外部單元。 固然,有時候一個用於注入的屬性必須在實例化時指定依賴單元。 這個時候,必然在其構造函數中有一個用於接收外部依賴單元的形式參數。 使用DI容器的目的是自動實例化,只是實例化而已,就意味着只須要調用構造函數。 至於setter注入能夠在實例化後操做嘛。
另外一個與解析依賴信息相關的方法就是 yii\di\Container::resolveDependencies() 。 它也是關乎 $_reflections 和 $_dependencies 數組的,它使用 yii\di\Container::getDependencies() 在這兩個數組中寫入的緩存信息,做進一步具體化的處理。從函數名來看,他的名字代表是用於解析依賴信息的。 下面咱們來看看它的代碼:
protected function resolveDependencies($dependencies, $reflection = null) { foreach ($dependencies as $index => $dependency) { // 前面getDependencies() 函數往 $_dependencies[] 中 // 寫入的是一個 Instance 數組 if ($dependency instanceof Instance) { if ($dependency->id !== null) { // 向容器索要所依賴的實例,遞歸調用 yii\di\Container::get() $dependencies[$index] = $this->get($dependency->id); } elseif ($reflection !== null) { $name = $reflection->getConstructor() ->getParameters()[$index]->getName(); $class = $reflection->getName(); throw new InvalidConfigException( "Missing required parameter \"$name\" when instantiating \"$class\"."); } } } return $dependencies; }
上面的代碼中能夠看到, yii\di\Container::resolveDependencies() 做用在於處理依賴信息, 將依賴信息中保存的Istance實例所引用的類或接口進行實例化。
綜合上面提到的 yii\di\Container::getDependencies() 和 yii\di\Container::resolveDependencies() 兩個方法,咱們能夠了解到:
解析完依賴信息,就萬事俱備了,那麼東風也該來了。實例的建立,祕密就在 yii\di\Container::build() 函數中
protected function build($class, $params, $config) { // 調用上面提到的getDependencies來獲取並緩存依賴信息,留意這裏 list 的用法 list ($reflection, $dependencies) = $this->getDependencies($class); // 用傳入的 $params 的內容補充、覆蓋到依賴信息中 foreach ($params as $index => $param) { $dependencies[$index] = $param; } // 這個語句是兩個條件: // 一是要建立的類是一個 yii\base\Object 類, // 留意咱們在《Yii基礎》一篇中講到,這個類對於構造函數的參數是有必定要求的。 // 二是依賴信息不爲空,也就是要麼已經註冊過依賴, // 要麼爲build() 傳入構造函數參數。 if (!empty($dependencies) && is_a($class, 'yii\base\Object', true)) { // 按照 Object 類的要求,構造函數的最後一個參數爲 $config 數組 $dependencies[count($dependencies) - 1] = $config; // 解析依賴信息,若是有依賴單元須要提早實例化,會在這一步完成 $dependencies = $this->resolveDependencies($dependencies, $reflection); // 實例化這個對象 return $reflection->newInstanceArgs($dependencies); } else { // 會出現異常的狀況有二: // 一是依賴信息爲空,也就是你前面又沒註冊過, // 如今又不提供構造函數參數,你讓Yii怎麼實例化? // 二是要構造的類,根本就不是 Object 類。 $dependencies = $this->resolveDependencies($dependencies, $reflection); $object = $reflection->newInstanceArgs($dependencies); foreach ($config as $name => $value) { $object->$name = $value; } return $object; } }
從這個 yii\di\Container::build() 來看:
與註冊依賴時使用 set() 和 setSingleton() 對應,獲取依賴實例化對象使用 yii\di\Container::get() ,其代碼以下:
public function get($class, $params = [], $config = []) { // 已經有一個完成實例化的單例,直接引用這個單例 if (isset($this->_singletons[$class])) { return $this->_singletons[$class]; // 是個還沒有註冊過的依賴,說明它不依賴其餘單元,或者依賴信息不用定義, // 則根據傳入的參數建立一個實例 } elseif (!isset($this->_definitions[$class])) { return $this->build($class, $params, $config); } // 注意這裏建立了 $_definitions[$class] 數組的副本 $definition = $this->_definitions[$class]; // 依賴的定義是個 PHP callable,調用之 if (is_callable($definition, true)) { $params = $this->resolveDependencies($this->mergeParams($class, $params)); $object = call_user_func($definition, $this, $params, $config); // 依賴的定義是個數組,合併相關的配置和參數,建立之 } elseif (is_array($definition)) { $concrete = $definition['class']; unset($definition['class']); // 合併將依賴定義中配置數組和參數數組與傳入的配置數組和參數數組合並 $config = array_merge($definition, $config); $params = $this->mergeParams($class, $params); if ($concrete === $class) { // 這是遞歸終止的重要條件 $object = $this->build($class, $params, $config); } else { // 這裏實現了遞歸解析 $object = $this->get($concrete, $params, $config); } // 依賴的定義是個對象則應當保存爲單例 } elseif (is_object($definition)) { return $this->_singletons[$class] = $definition; } else { throw new InvalidConfigException( "Unexpected object definition type: " . gettype($definition)); } // 依賴的定義已經定義爲單例的,應當實例化該對象 if (array_key_exists($class, $this->_singletons)) { $this->_singletons[$class] = $object; } return $object; }
get() 用於返回一個對象或一個別名所表明的對象。能夠是已經註冊好依賴的,也能夠是沒有註冊過依賴的。 不管是哪一種狀況,Yii均會自動解析將要獲取的對象對外部的依賴。
get() 接受3個參數:
get() 解析依賴獲取對象是一個自動遞歸的過程,也就是說,當要獲取的對象依賴於其餘對象時, Yii會自動獲取這些對象及其所依賴的下層對象的實例。 同時,即便對於未定義的依賴,DI容器經過PHP的Reflection API,也能夠自動解析出當前對象的依賴來。
get() 不直接實例化對象,也不直接解析依賴信息。而是經過 build() 來實例化對象和解析依賴。
get() 會根據依賴定義,遞歸調用自身去獲取依賴單元。 所以,在整個實例化過程當中,一共有兩個地方會產生遞歸:一是 get() , 二是 build() 中的 resolveDependencies() 。
DI容器解析依賴實例化對象過程大致上是這麼一個流程:
從 get() 的代碼能夠看出:
爲了加深理解,咱們以官方文檔上的例子來講明DI容器解析依賴的過程。假設有如下代碼:
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); } public function findUser() { } } class UserLister extends Object { public $finder; // 從構造函數看,這個類依賴於 UserFinderInterface接口 public function __construct(UserFinderInterface $finder, $config = []) { $this->finder = $finder; parent::__construct($config); } }
從依賴關係看,這裏的 UserLister 類依賴於接口 UserFinderInterface , 而接口有一個實現就是 UserFinder 類,但這類又依賴於 Connection 。
那麼,按照通常常規的做法,要實例化一個 UserLister 一般這麼作:
$db = new \yii\db\Connection(['dsn' => '...']); $finder = new UserFinder($db); $lister = new UserLister($finder);
就是逆着依賴關係,從最底層的 Connection 開始實例化,接着是 UserFinder 最後是 UserLister 。 在寫代碼的時候,這個先後順序是不能亂的。並且,須要用到的單元,你要本身一個一個提早準備好。 對於本身寫的可能還比較清楚,對於其餘團隊成員寫的,你還要看他的類到底是依賴了哪些,並一一實例化。 這種狀況,若是是個別的、少許的還能夠接受,若是有個10-20個的,那就麻煩了。 估計光實例化的代碼,就能夠寫滿一屏幕了。
並且,若是是團隊開發,有些單元應當是共用的,如郵件投遞服務。 不能說你寫個模塊,要用到郵件服務了,就本身實例化一個郵件服務吧?那樣豈不是有N模塊就有N個郵件服務了? 最好的方式是使郵件服務成爲一個單例,這樣任何模塊在須要郵件服務時,使用的實際上是同一個實例。 用傳統的這種實例化對象的方法來實現的話,就沒那麼直接了。
那麼改爲DI容器的話,應該是怎麼樣呢?他是這樣的:
use yii\di\Container; // 建立一個DI容器 $container = new Container; // 爲Connection指定一個數組做爲依賴,當須要Connection的實例時, // 使用這個數組進行建立 $container->set('yii\db\Connection', [ 'dsn' => '...', ]); // 在須要使用接口 UserFinderInterface 時,採用UserFinder類實現 $container->set('app\models\UserFinderInterface', [ 'class' => 'app\models\UserFinder', ]); // 爲UserLister定義一個別名 $container->set('userLister', 'app\models\UserLister'); // 獲取這個UserList的實例 $lister = $container->get('userLister');
採用DI容器的辦法,首先各 set() 語句沒有先後關係的要求, set() 只是寫入特定的數據結構, 並未涉及具體依賴關係的解析。因此,先後關係不重要,先定義什麼依賴,後定義什麼依賴沒有關係。
其次,上面根本沒有在DI容器中定義 UserFinder 對於 Connection 的依賴。 可是DI容器經過對 UserFinder 構造函數的分析,能瞭解到這個類會對 Connection 依賴。這個過程是自動的。
最後,上面只有一個 get() 看起來好像根本沒有實例化其餘如 Connection 單元同樣,但事實上,DI容器已經安排好了一切。 在獲取 userLister 以前, Connection 和 UserFinder 都會被自動實例化。 其中, Connection 是根據依賴定義中的配置數組進行實例化的。
通過上面的幾個 set() 語句以後,DI容器的 $_params 數組是空的, $_singletons 數組也是空的。 $_definintions 數組卻有了新的內容:
$_definitions = [ 'yii\db\Connection' => [ 'class' => 'yii\db\Connection', // 注意這裏 'dsn' => ... ], 'app\models\UserFinderInterface' => ['class' => 'app\models\UserFinder'], 'userLister' => ['class' => 'app\models\UserLister'] // 注意這裏 ];
在調用 get('userLister') 過程當中又發生了什麼呢?說實話,這個過程不是十分複雜, 可是因爲涉及到遞歸和回溯,寫這裏的時候,我寫了改,改了寫,示意圖畫了好幾次,折騰了很久,都不滿意, 就怕說不清楚,讀者朋友們理解起來費勁。 最後畫了一個簡單的示意圖,請大家對照 DI容器解析依賴獲取實例的過程示意圖 , 以及前面關於 get() build() getDependencies() resolveDependencies() 等函數的源代碼, 瞭解大體流程。若是有任何疑問、建議,也請在底部留言。
在 DI容器解析依賴獲取實例的過程示意圖 中綠色方框表示DI容器的5個數組,淺藍色圓邊方框表示調用的函數和方法。 藍色箭頭表示讀取內存,紅色箭頭表示寫入內存,虛線箭頭表示參照的內存對象,粗線綠色箭頭表示回溯過程。 圖中3個圓柱體表示實例化過程當中,建立出來的3個實例。
對於 get() 函數:
build() 在實例化過程當中,幹了這麼幾件事:
getDependencies() 函數老是被 build() 調用,他幹了這麼幾件事:
resolveDependencies() 函數老是被 build() 調用,他在實例化時,幹了這麼幾件事:
newInstanceArgs() 函數是PHP Reflection API的函數,用於建立實例,具體請看 PHP手冊 。
這裏只是簡單的舉例子而已,尚未涉及到多依賴和單例的情形,可是在原理上是同樣的。 但願繼續深刻了解的讀者朋友能夠再看看上面有關函數的源代碼就好了,有疑問請隨時留言。
從上面的例子中不難發現,DI容器維護了兩個緩存數組 $_reflections 和 $_dependencies 。這兩個數組只寫入一次,就能夠無限次使用。 所以,減小了對ReflectionClass的使用,提升了DI容器解析依賴和獲取實例的效率。
另外一方面,咱們看到,獲取一個實例,步驟其實很多。可是,對於典型的Web應用而言, 有許多模塊其實應當註冊爲單例的,好比上面的 yii\db\Connection 。 一個Web應用通常使用一個數據庫鏈接,特殊狀況下會用多幾個,因此這些數據庫鏈接通常是給定不一樣別名加以區分後, 分別以單例形式放在容器中的。所以,實際獲取實例時,步驟會簡單得。對於單例, 在第一次 get() 時,直接就返回了。並且,省去不重複構造實例的過程。
這兩個方面,都體現出Yii高效能的特色。
上面咱們分析了DI容器,這只是其中的原理部分,具體的運用,咱們將結合 服務定位器(Service Locator) 來說。
轉載自:http://www.digpage.com/di.html