前言
有這樣一種容器,它存放的是對象、對象的描述(類、接口)或者是提供對象的回調,通過這種容器,我們得以實作許多進階的功能,其中最常提到的,就是 “解耦” 、“依賴注入(DI)”。本文就從這裡開始。
IoC 容器, laravel 的核心
Laravel(音 [‘lærəvel],”來若外偶“)的核心就是一個 IoC 容器 ,根據文檔,稱其為“ 服務容器 ”,顧名思義,該容器提供了整個架構中需要的一系列服務。作為初學者,很多人會在這一個概念上犯難,是以,我打算從一些基礎的内容開始講解,通過了解面向對象開發中依賴的産生和解決方法,來逐漸揭開“依賴注入”的面紗,逐漸了解這一神奇的設計理念。
本文一大半内容都是通過舉例來讓讀者去了解什麼是 IoC(控制反轉) 和 DI(依賴注入),通過了解這些概念,來更加深入。更多關于 laravel 服務容器的用法建議閱讀文檔即可。
IoC 容器誕生的故事
面向對象程式設計,有以下幾樣東西無時不刻的接觸:接口、類還有對象。這其中,接口是類的原型,一個類必須要遵守其實作的接口;對象則是一個類執行個體化後的産物,我們稱其為一個執行個體。當然這樣說肯定不利于了解,我們就實際的寫點中看不中用的代碼輔助學習。
怪物橫行的世界,總歸需要點超級人物來擺平。
我們把一個“超人”作為一個類,
class Superman {}
我們可以想象,一個超人誕生的時候肯定擁有至少一個超能力,這個超能力也可以抽象為一個對象,為這個對象定義一個描述他的類吧。一個超能力肯定有多種屬性、(操作)方法,這個盡情的想象,但是目前我們先大緻定義一個隻有屬性的“超能力”,至于能幹啥,我們以後再豐富:
class Power {
/**
* 能力值
*/
protected $ability;
/**
* 能力範圍或距離
*/
protected $range;
public function __construct($ability, $range)
{
$this->ability = $ability;
$this->range = $range;
}
}
這時候我們回過頭,修改一下之前的“超人”類,讓一個“超人”建立的時候被賦予一個超能力:
class Superman
{
protected $power;
public function __construct()
{
$this->power = new Power(, );
}
}
這樣的話,當我們建立一個“超人”執行個體的時候,同時也建立了一個“超能力”的執行個體,但是,我們看到了一點,“超人”和“超能力”之間不可避免的産生了一個依賴。
在一個貫徹面向對象程式設計的項目中,這樣的依賴随處可見。少量的依賴并不會有太過直覺的影響,我們随着這個例子逐漸鋪開,讓大家慢慢意識到,當依賴達到一個量級時,是怎樣一番噩夢般的體驗。當然,我也會自然而然的講述如何解決問題。
一堆亂麻 —— 可怕的依賴
之前的例子中,超能力類執行個體化後是一個具體的超能力,但是我們知道,超人的超能力是多元化的,每種超能力的方法、屬性都有不小的差異,沒法通過一種類描述完全。我們現在進行修改,我們假設超人可以有以下多種超能力:
飛行,屬性有:飛行速度、持續飛行時間
蠻力,屬性有:力量值
能量彈,屬性有:傷害值、射擊距離、同時射擊個數
我們建立了如下類:
class Flight
{
protected $speed;
protected $holdtime;
public function __construct($speed, $holdtime) {//...}
}
class Force
{
protected $force;
public function __construct($force) {//...}
}
class Shot
{
protected $atk;
protected $range;
protected $limit;
public function __construct($atk, $range, $limit) {//...}
}
好了,這下我們的超人有點“忙”了。在超人初始化的時候,我們會根據需要來執行個體化其擁有的超能力嗎,大緻如下
class Superman
{
protected $power;
public function __construct()
{
$this->power = new Fight(, );
// $this->power = new Force(45);
// $this->power = new Shot(99, 50, 2);
/*
$this->power = array(
new Force(45),
new Shot(99, 50, 2)
);
*/
}
}
我們需要自己手動的在構造函數内(或者其他方法裡)執行個體化一系列需要的類,這樣并不好。可以想象,假如需求變更(不同的怪物橫行地球),需要更多的有針對性的 新的 超能力,或者需要 變更 超能力的方法,我們必須 重新改造 超人。換句話說就是,改變超能力的同時,我還得重新制造個超人。效率太低了!
這時,靈機一動的人想到:為什麼不可以這樣呢?超人的能力可以被随時更換,隻需要添加或者更新一個晶片或者其他裝置啥的(想到鋼鐵俠沒)。這樣的話就不要整個重新來過了。
對,就是這樣的。
我們不應該手動在 “超人” 類中固化了他的 “超能力” 初始化的行為,而轉由外部負責,由外部創造超能力模組、裝置或者晶片等(我們後面統一稱為 “模組”),植入超人體内的某一個接口,這個接口是一個既定的,隻要這個 “模組” 滿足這個接口的裝置都可以被超人所利用,可以提升、增加超人的某一種能力。這種由外部負責其依賴需求的行為,我們可以稱其為 “ 控制反轉 (IoC) ”。
工廠模式,依賴轉移
當然,實作控制反轉的方法有幾種。在這之前,不如我們先了解一些好玩的東西。
我們可以想到,元件、工具(或者超人的模組),是一種可被生産的玩意兒,生産的地方當然是 “工廠(Factory)”,于是有人就提出了這樣一種模式: 工廠模式 。
我們為了給超人制造超能力模組,我們建立了一個工廠,它可以制造各種各樣的模組,且僅需要通過一個方法:
class SuperModuleFactory
{
public function makeModule($moduleName, $options)
{
switch ($moduleName) {
case 'Fight': return new Fight($options[], $options[]);
case 'Force': return new Force($options[]);
case 'Shot': return new Shot($options[], $options[], $options[]);
}
}
}
這時候,超人 建立之初就可以使用這個工廠!
class Superman
{
protected $power;
public function __construct()
{
// 初始化工廠
$factory = new SuperModuleFactory;
// 通過工廠提供的方法制造需要的子產品
$this->power = $factory->makeModule('Fight', [, ]);
// $this->power = $factory->makeModule('Force', [45]);
// $this->power = $factory->makeModule('Shot', [99, 50, 2]);
/*
$this->power = array(
$factory->makeModule('Force', [45]),
$factory->makeModule('Shot', [99, 50, 2])
);
*/
}
}
可以看得出,我們不再需要在超人初始化之初,去初始化許多第三方類,隻需初始化一個工廠類,即可滿足需求。但這樣似乎和以前差別不大,隻是沒有那麼多 new 關鍵字。其實我們稍微改造一下這個類,你就明白,工廠類的真正意義和價值了。
class Superman
{
protected $power;
public function __construct(array $modules)
{
// 初始化工廠
$factory = new SuperModuleFactory;
// 通過工廠提供的方法制造需要的子產品
foreach ($modules as $moduleName => $moduleOptions) {
$this->power[] = $factory->makeModule($moduleName, $moduleOptions);
}
}
}
// 建立超人
$superman = new Superman([
'Fight' => [, ],
'Shot' => [, , ]
]);
現在修改的結果令人滿意。現在,“超人” 的建立不再依賴任何一個 “超能力” 的類,我們如若修改了或者增加了新的超能力,隻需要針對修改 SuperModuleFactory 即可。擴充超能力的同時不再需要重新編輯超人的類檔案,使得我們變得很輕松。但是,這才剛剛開始。
再進一步!IoC 容器的重要組成 —— 依賴注入
大多數情況下,工廠模式已經足夠了。工廠模式的缺點就是:接口未知(即沒有一個很好的契約模型,關于這個我馬上會有解釋)、産生對象類型單一。總之就是,還是不夠靈活。雖然如此,工廠模式依舊十分優秀,并且适用于絕大多數情況。不過我們為了講解後面的 依賴注入 ,這裡就先誇大一下工廠模式的缺陷咯。
我們知道,超人依賴的模組,我們要求有統一的接口,這樣才能和超人身上的注入接口對接,最終起到提升超能力的效果。
事實上,我之前說謊了,不僅僅隻有一堆小怪獸,還有更多的大怪獸。嘿嘿。額,這時候似乎工廠的生産能力顯得有些不足 —— 由于工廠模式下,所有的模組都已經在工廠類中安排好了,如果有新的、進階的模組加入,我們必須修改工廠類(好比增加新的生産線):
class SuperModuleFactory
{
public function makeModule($moduleName, $options)
{
switch ($moduleName) {
case 'Fight': return new Fight($options[], $options[]);
case 'Force': return new Force($options[]);
case 'Shot': return new Shot($options[], $options[], $options[]);
// case 'more': .......
// case 'and more': .......
// case 'and more': .......
// case 'oh no! its too many!': .......
}
}
}
看到沒。。。噩夢般的感受!
你可能會想到更為靈活的辦法!對,下一步就是我們今天的主要配角 —— DI (依賴注入)
由于對超能力模組的需求不斷增大,我們需要集合整個世界的高智商人才,一起解決問題,不應該僅僅隻有幾個工廠壟斷負責。不過高智商人才們都非常自負,認為自己的想法是對的,創造出的超能力模組沒有統一的接口,自然而然無法被正常使用。這時我們需要提出一種契約,這樣無論是誰創造出的模組,都符合這樣的接口,自然就可被正常使用。
interface SuperModuleInterface
{
/**
* 超能力激活方法
*
* 任何一個超能力都得有該方法,并擁有一個參數
*@param array $target 針對目标,可以是一個或多個,自己或他人
*/
public function activate(array $target);
}
上文中,我們定下了一個接口 (超能力模組的規範、契約),所有被創造的模組必須遵守該規範,才能被生産。
其實,這就是 php 中 接口( interface ) 的用處和意義!很多人覺得,為什麼 php 需要接口這種東西?難道不是 java 、
C# 之類的語言才有的嗎?這麼說,隻要是一個正常的面向對象程式設計語言(雖然 php 可以面向過程),都應該具備這一特性。因為一個
對象(object) 本身是由他的模闆或者原型 —— 類 (class)
,經過執行個體化後産生的一個具體事物,而有時候,實作統一種方法且不同功能(或特性)的時候,會存在很多的類(class),這時候就需要有一個契約,讓大家編寫出可以被随時替換卻不會産生影響的接口。這種由程式設計語言本身提出的硬性規範,會增加更多優秀的特性。
這時候,那些提出更好的超能力模組的高智商人才,遵循這個接口,建立了下述(模組)類:
/**
* X-超能量
*/
class XPower implements SuperModuleInterface
{
public function activate(array $target)
{
// 這隻是個例子。。具體自行腦補
}
}
/**
* 終極炸彈 (就這麼俗)
*/
class UltraBomb implements SuperModuleInterface
{
public function activate(array $target)
{
// 這隻是個例子。。具體自行腦補
}
}
同時,為了防止有些 “磚家” 自作聰明,或者一些叛徒惡意搗蛋,不遵守契約胡亂制造模組,影響超人,我們對超人初始化的方法進行改造
class Superman
{
protected $module;
public function __construct(SuperModuleInterface $module)
{
$this->module = $module
}
}
改造完畢!現在,當我們初始化 “超人” 類的時候,提供的模組執行個體必須是一個 SuperModuleInterface 接口的實作。否則就會提示錯誤。
本文從開頭到現在提到的一系列依賴,隻要不是由内部生産(比如初始化、構造函數 __construct 中通過工廠方法、自行手動 new 的),而是由外部以參數或其他形式注入的,都屬于 依賴注入(DI) 。是不是豁然開朗?事實上,就是這麼簡單。下面就是一個典型的 依賴注入 :
// 超能力模組
$superModule = new XPower;
// 初始化一個超人,并注入一個超能力模組依賴
$superMan = new Superman($superModule);