爲了更好的理解依賴注入 (DI) 和 IOC 容器的概念,咱們先設計一個場景。如今你餓了,準備要享用一個晚餐,那麼你可能要作的事情有購買食材,烹飪食材,享用食物。php
晚餐的類設計看起來應該像是這樣的:設計模式
<?php namespace Ioc; class Dinner { public function buyFood() { // } public function cookFood() { // } public function eatFood() { // } }
單拿 cookFood 這步來講,你可能還須要一種能源資源,以便將食材加熱,比方說,你選擇了燃氣。那麼燃氣的類設計看起來應該像是這樣的:this
<?php namespace Ioc; class Gas { public function fire() { echo __CLASS__."::".__FUNCTION__.PHP_EOL; } }
好了,如今能夠用燃氣來加熱了。spa
... class Dinner { ... public function cookFood() { $gas = new Gas(); $gas->fire(); } ... }
爲節省篇幅,以上代碼使用了 ‘…’ 來隱藏了部分代碼,如下文章狀況相似。那麼調用過程是這樣的:設計
$dinner = new \Ioc\Dinner(); $dinner->cookFood();
以上的設計就產生了依賴了,Dinner 依賴了 Gas ,這種依賴讓兩個類耦合在一塊兒,這種設計的缺陷是明顯的。萬一燃氣用光了呢,萬一由天燃氣改爲煤氣了呢,那樣子晚餐就泡湯了。在代碼看來就是,一旦 Gas 類在某些環境下不能運做了,一旦 Gas 要更改類名了,那麼 Dinner 會很被動,何況每一次調用都要 new 實例化一次 Gas ,這很浪費系統資源。代理
IOC 全稱是 Inversion of Control,譯做控制反轉。像以上設計,Dinner 稱做主類, Gas 稱做次類, 次類的實例化由主類來控制,這種方式就是正向的控制,若是次類的實例化並不禁主類來控制的話,大概就是控制反轉的意思了。code
怎麼解決這種強耦合關係?一種解決方式是使用工廠模式。對象
工廠模式資源
工廠模式很簡單,就是使用一個代理類來幫助你批量實例化「次類」。
Agent 類以下:get
<?php namespace Ioc; class Agent { public static function useEnergy() { return new Gas(); } }
Dinner 類以下:
... class Dinner { protected $energy; ... public function cookFood() { $this->energy = Agent::useEnergy(); $this->energy->fire(); } ... }
如此,便可使 Dinner 再也不直接依賴 Gas,而交由一個代理 Agent 來控制 energy 的建立。然而,Gas 依賴解除了,又帶來了 Agent 的依賴,雖然 Agent 的更改可能性不太,但誰能保證呢。
依賴注入 (DI)
在完全解除依賴,必需要將次類的調用代碼從主類中移除才行,不然次類像更改類名這樣的改動都將牽動着所在全部依賴它的主類的代碼,全部依賴它的主類都要跟着改代碼,可謂牽一髮而動全身。
一種依賴注入的方式就是,被依賴的對象經過參數從外部注入到類內部。更改 Dinner 類以下:
... public function setEnergy($energy) { $this->energy = $energy; } public function cookFood() { $this->energy->fire(); } ...
添加一個 setEnergy 方法來注入依賴的對象。那麼調用過程將變成:
$dinner = new \Ioc\Dinner(); $dinner->setEnergy(\Ioc\Agent::useEnergy()); $dinner->cookFood();
以上就是一種依賴注入的示例。Dinner 完全解除了對能源類的依賴。
可是新問題還會產生,cookFood 並不僅依賴能源,可能還依賴廚具,調味料等。那麼調用過程將會是這樣的:
$dinner->setEnergy(...); $dinner->setKitchen(...); $dinner->setSauce(...); $dinner->cookFood();
每次都要調用不少 set 方法,這樣就更不科學了。與其這樣,乾脆全部 set 方法都交給一個 TopAgent 作好了。
TopAgent 類以下:
<?php namespace Ioc; class TopAgent { public static function setAllDi() { $dinner = new Dinner(); $dinner->setEnergy(Agent::useEnergy()); $dinner->setKitchen(Agent::useKitchen()); $dinner->setSauce(Agent::useSauce()); return $dinner; } }
這樣,調用過程就變得簡單了。
到目前爲止,基本上已實現了 Dinner 的依賴注入了。可認真一看,瞬間,彷佛又回到了最初的問題了,不,不是彷佛,簡直就是了! Dinner 類是解除了外部類的依賴了,但它本身卻成了 TopAgent 的依賴類了,而 TopAgent 不正是最初的 Dinner 了嗎!繞了一大圈,原來還在原點,一次又一次,咱們又回到了不實用的例子中來了。
一個實用和優雅的解決方法,是爲依賴實例提供一個容器。便是 IOC 容器。
IOC 容器
IOC 容器首先是一種類註冊器,其次它是一種更高級的依賴注入方式。它和工廠 Factory 其實性質同樣,代理類,但實現機制不同。
IOC 容器的設計模式叫作註冊器模式。
Container 類以下:
<?php namespace Ioc; class Container { protected static $objects = []; public static function set($key, $object) { self::$objects[$key] = $object; } public static function get($key){ $closure = self::$objects[$key]; return $closure(); } }
Agent 類再添加兩個方法:
... public static function bindContainer() { return new Container(); } public static function bindDinner(Container $container) { return new Dinner($container); } ...
Dinner 類接受一個 Container 注入:
<?php namespace Ioc; class Dinner { protected $container; public function __construct(Container $container){ $this->container = $container; } public function buyFood() { // } public function cookFood() { $this->container->get('energy')->fire(); } public function eatFood() { // } }
因而,調用過程即可漂亮的寫成:
\Ioc\Container::set('energy', function () { return \Ioc\Agent::useEnergy(); }); $dinner = \Ioc\Agent::bindDinner(\Ioc\Agent::bindContainer()); $dinner->cookFood();
將容器 Container 注入到 Dinner 。並實現了全部類的徹底解耦。