為了更好的理解依賴注入 (DI) 和 IOC 容器的概念,我們先設計一個場景。現在你餓了,準備要享用一個晚餐,那麼你可能要做的事情有購買食材,烹飪食材,享用食物。
晚餐的類設計看起來應該像是這樣的:
<?php
namespace Ioc;
class Dinner
{
public function buyFood()
{
//
}
public function cookFood()
{
//
}
public function eatFood()
{
//
}
}
單拿 cookFood 這步來說,你可能還需要一種能源資源,以便將食材加熱,比方說,你選擇了燃氣。那麼燃氣的類設計看起來應該像是這樣的:
<?php
namespace Ioc;
class Gas
{
public function fire()
{
echo __CLASS__."::".__FUNCTION__.PHP_EOL;
}
}
好了,現在可以用燃氣來加熱了。
...
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 稱作次類, 次類的例項化由主類來控制,這種方式就是正向的控制,如果次類的例項化並不由主類來控制的話,大概就是控制反轉的意思了。
怎麼解決這種強耦合關係?一種解決方式是使用工廠模式。
工廠模式
工廠模式很簡單,就是使用一個代理類來幫助你批次例項化“次類”。
Agent 類如下:
<?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 。並實現了所有類的完全解耦。
本作品採用《CC 協議》,轉載必須註明作者和本文連結