什麼是依賴注入?
IOC:英文全稱:Inversion of Control,中文名稱:控制反轉,它還有個名字叫依賴注入(Dependency Injection,簡稱DI)。
當一個類的實例需要另一個類的實例協助時,在傳統的程序設計過程中,通常由調用者來創建被調用者的實例。而採用依賴注入的方式,創建被調用者的工作不再由調用者來完成,因此叫控制反轉,創建被調用者的實例的工作由IOC容器來完成,然後注入調用者,因此也稱爲依賴注入。
舉個簡單的例子:
(1)原始社會裏,幾乎沒有社會分工。需要斧子的人(調用者)只能自己去磨一把斧子(被調用者)。
(2)進入工業社會,工廠出現。斧子不再由普通人完成,而在工廠裏被生產出來,此時需要斧子的人(調用者)找到工廠,購買斧子,無須關心斧子的製造過程。
(3)進入“按需分配”社會,需要斧子的人不需要找到工廠,坐在家裏發出一個簡單指令:需要斧子。斧子就自然出現在他面前。
第一種情況下,實例的調用者創建被調用的實例,必然要求被調用的類出現在調用者的代碼裏。無法實現二者之間的鬆耦合。
第二種情況下,調用者無須關心被調用者具體實現過程,只需要找到符合某種標準(接口)的實例,即可使用。此時調用的代碼面向接口編程,可以讓調用者和被調用者解耦,這也是工廠模式大量使用的原因。但調用者需要自己定位工廠,調用者與特定工廠耦合在一起。
第三種情況下,調用者無須自己定位工廠,程序運行到需要被調用者時,依賴注入容器自動提供被調用者實例。事實上,調用者和被調用者都處於依賴注入容器的管理下,二者之間的依賴關係由依賴注入容器提供。因此調用者與被調用者的耦合度進一步降低,這使得應用更加容易維護,這就是依賴注入所要達到的目的。
用php實現一個輕量的依賴注入容器
首先我們創建一個類,看起來是這樣的:
<?php
class Di
{
protected $_service = [];
public function set($name, $definition)
{
$this->_service[$name] = $definition;
}
public function get($name)
{
if (isset($this->_service[$name])) {
$definition = $this->service[$name];
} else {
throw new Exception("Service '" . name . "' wasn't found in the dependency injection container");
}
if (is_object($definition)) {
$instance = call_user_func($definition);
}
return $instance;
}
}
現在我們已經有了一個簡單的類,包含一個屬性和兩個方法。假設我們現在有兩個類,redisDB和cache,redisDB提供一個redis數據庫的操作,cache負責緩存功能的實現並且依賴於redisDB。
class redisDB
{
protected $_di;
protected $_options;
public function __construct($options = null)
{
$this->_options = $options;
}
public function setDI($di)
{
$this->_di = $di;
}
public function find($key, $lifetime)
{
// code
}
public function save($key, $value, $lifetime)
{
// code
}
public function delete($key)
{
// code
}
}
在這個類中我們簡單實現了redis的查詢、保存和刪除。你可能會有疑問,另外一個方法setDi是做什麼的。待我繼續爲你講解。另一個類和當前這個類結構很像:
class cache
{
protected $_di;
protected $_options;
protected $_connect;
public function __construct($options = null)
{
$this->_options = $options;
}
public function setDI($di)
{
$this->_di = $di;
}
protected function _connect()
{
$options = $this->_options;
if (isset($options['connect'])) {
$service = $options['connect'];
} else {
$service = 'redis';
}
return $this->_di->get($service);
}
public function get($key, $lifetime)
{
$connect = $this->_connect;
if (!is_object($connect)) {
$connect = $this->_connect()
$this->_connect = $connect;
}
// code
...
return $connect->find($key, $lifetime);
}
public function save($key, $value, $lifetime)
{
$connect = $this->_connect;
if (!is_object($connect)) {
$connect = $this->_connect()
$this->_connect = $connect;
}
// code
...
return $connect->save($key, $lifetime);
}
public function delete($key)
{
$connect = $this->_connect;
if (!is_object($connect)) {
$connect = $this->_connect()
$this->_connect = $connect;
}
// code
...
$connect->delete($key, $lifetime);
}
}
現在我們就當已經實現了redisDB和cache這兩個組件,具體的細節這裏就先不做討論了,來看看如何使用使用吧。首先需要將兩個組件注入到容器中:
<?php
$di = new Di();
$di->set('redis', function() {
return new redisDB([
'host' => '127.0.0.1',
'port' => 6379
]);
});
$di->set('cache', function() use ($di) {
$cache = new cache([
'connect' => 'redis'
]);
$cache->setDi($di);
return $cache;
});
// 然後在任何你想使用cache的地方
$cache = $di->get('cache');
$cache->get('key'); // 獲取緩存數據
$cache->save('key', 'value', 'lifetime'); // 保存數據
$cache->delete('key'); // 刪除數據
到這裏你可能會覺得這樣以來反而有點繁瑣了。cache和redisDB的結構如此之像,完全可以把redis寫到cache中而沒必要單獨分離出來?
但是你想過沒有,有些數據及時性沒那麼高而且數量比較大,用redis有點不合適,mongodb是更好的選擇;有些數據更新頻率更慢,對查詢速度也沒要求,直接寫入文件保存到硬盤可能更爲合適;再或者,你的客戶覺得redis運維難度有點大,讓你給他換成memcache… 這就是爲什麼把它分離出來了。然後,繼續改進代碼:
interface BackendInterface {
public function find($key, $lifetime);
public function save($key, $value, $lifetime);
public function delete($key);
}
class redisDB implements BackendInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
class mongoDB implements BackendInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
class file implements BackendInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
$di = new Di();
// redis
$di->set('redis', function() {
return new redisDB([
'host' => '127.0.0.1',
'port' => 6379
]);
});
// mongodb
$di->set('mongo', function() {
return new mongoDB([
'host' => '127.0.0.1',
'port' => 12707
]);
});
// file
$di->set('file', function() {
return new file([
'path' => 'path'
]);
});
// save at redis
$di->set('fastCache', function() use ($di) {
$cache = new cache([
'connect' => 'redis'
]);
$cache->setDi($di);
return $cache;
});
// save at mongodb
$di->set('cache', function() use ($di) {
$cache = new cache([
'connect' => 'mongo'
]);
$cache->setDi($di);
return $cache;
});
// save at file
$di->set('slowCache', function() use ($di) {
$cache = new cache([
'connect' => 'file'
]);
$cache->setDi($di);
return $cache;
});
// 然後在任何你想使用cache的地方
$cache = $di->get('cache');
我們新增加了一個接口BackendInterface,規定了redisDB,mongoDB,file這三個類必須實現這個接口所要求的功能,至於其他錦上添花的功能,隨你怎麼發揮。而cache的代碼,好像沒有變,因爲cache不需要關心數據是怎麼存入數據庫或者文件中。而cache的調用者,也不需要關心cache具體是怎麼實現的,只要根據接口實現相應的方法就行了。多人協作你會更加受益,你們只需要商定好接口,然後分別實現就行了。
這就是依賴注入的魅力所在了,雖然看似如此簡單。
以上代碼還可以繼續改進,直到你認爲無可挑剔爲止。比如,redis服務在一個請求中可能會調用多次,而每次調用都會重新創建,這將有損性能。只需擴展一下DI容器就好增加一個參數或增加一個方法,隨你。
class Di
{
protected $_service = [];
protected $_sharedService = [];
public function set($name, $definition, $shared = false)
{
if ($shared) {
$this->_sharedService[$name] = $definition;
} else {
$this->_service[$name] = $definition;
}
}
public function get($name) {
if (isset($this->_service[$name])) {
$definition = $this->service[$name];
} else if ($this->_sharedService[$name]) {
$definition = $this->_sharedService[$name];
} else {
throw new Exception("Service '" . name . "' wasn't found in the dependency injection container");
}
...
}
這樣以來,如果某個服務在一次請求中要調用多次,你就可以將shared屬性設置爲true,以減少不必要的浪費。如果你覺得每次在注入時都要setDi有點繁瑣,想讓他自動setDi,那可以這麼做:
interface DiAwareInterface
{
public function setDI($di);
public function getDI();
}
class Di
{
protected $service;
public function set($name, $definition)
{
$this->service[$name] = $definition;
}
public function get($name)
{
...
if (is_object($definition)) {
$instance = call_user_func($definition);
}
// 如果實現了DiAwareInterface這個接口,自動注入
if (is_object($instance)) {
if ($instance instanceof DiAwareInterface) {
$instance->setDI($this);
}
}
return $instance;
}
}
class redisDB implements BackendInterface, DiAwareInterface
{
public function find($key, $lifetime) { }
public function save($key, $value, $lifetime) { }
public function delete($key) { }
}
然後,就可以這樣:
$di->set('cache', function() {
return new cache([
'connect' => 'mongo'
]);
});
我們現在所實現的這個DI容器還很簡陋,還不支持複雜的注入,你可以繼續完善它。
不過,通過這些代碼你已經瞭解什麼是依賴在注入了,你可以將這種思想應用到你的項目中,或者着手開發你自己的框架。