本文翻譯自Dependency Injection: Huh?。 眾所周知,Laravel的核心功能包括依賴注入和控制反轉。在我看過的很多與這些功能相關的文章裡,很少有生動形象的解釋這兩個功能的。這篇文章解決了我的疑惑,雖然這是一篇比較古老的文章了。但是還是想翻譯出來和大家分享。
在我們學習寫程式碼的過程中,肯定會碰到有人說一個概念——"依賴注入",如果你是一個初學者的話, 肯定會對這個概念感到困惑,並且會跳過這一部分。但是,這個功能對於編寫可維護性(和可測試性)程式碼來說是必不可少的一個功能。在這篇文章中,我會將我對依賴注入的理解按照最簡單的方法向大家進行闡述。
舉個例子
讓我們直接上程式碼
class Photo {
/**
* @var PDO The connection to the database
*/
protected $db;
/**
* Construct.
*/
public function __construct()
{
$this->db = DB::getInstance();
}
}
第一次看這段程式碼我們可能覺得基本還可以。實際上,我們已經寫死了一段依賴,即資料庫連線。如果我們想換一個連線層呢?或者說,為什麼 Photo
物件要與外面的內容有關聯呢?事實上,確實如此。這個物件應當只關聯 Photo
相關的內容。
基本的理念是你的類應當只處理一件事情。所以 Photo
物件不應當處理資料庫的連線。
你可以把物件理解成你家的寵物。你的狗不會自己決定出去散步或者是在公園玩耍。是你決定的!這不是它做決定的地方。
讓我們最佳化這段程式碼,有兩種方式可以達成我們的目標,構造注入和 setter 注入。下面是對應的程式碼
構造注入
class Photo {
/**
* @var PDO The connection to the database
*/
protected $db;
/**
* Construct.
* @param PDO $db_conn The database connection
*/
public function __construct($dbConn)
{
$this->db = $dbConn;
}
}
$photo = new Photo($dbConn);
在上面的程式碼中, 我們在構造方法中將所有的依賴注入。
setter注入
class Photo {
/**
* @var PDO The connection to the database
*/
protected $db;
public function __construct() {}
/**
* Sets the database connection
* @param PDO $dbConn The connection to the database.
*/
public function setDB($dbConn)
{
$this->db = $dbConn;
}
}
$photo = new Photo;
$photo->setDB($dbConn);
經過我們簡單的修改,這個類不在依賴任何的連線。雖然目前看來不是很明顯,但是這個技巧能讓我們更方便的進行測試,因為我們現在可以在呼叫setDB
的時候mock資料庫。
更好的是,如果我們想換一個不同的連線,由於我們使用了依賴注入,這會是一個非常簡單的事情。
問題浮現
使用 Setter 注入會有一個問題——會使我們的類依賴越來越難以管理。使用者必須時刻記住這個類都用到了哪些依賴以及在使用時要一一進行 set 設定。所以,在我們依賴的內容越來越多的時候,我們就會看到下面的情況:
$photo = new Photo;
$photo->setDB($dbConn);
$photo->setConfig($config);
$photo->setResponse($response);
解決方案
解決方案很簡單,就是建立一個容器類來替我們處理這些依賴問題。是的,這裡就是你經常聽到的控制反轉(Inversion of Control)——IoC。
定義:在軟體工程中,控制反轉(IoC)是物件導向程式設計中的一種設計原則,可以用來減低計算機程式碼之間的耦合度。透過控制反轉,物件在被建立的時候,由一個調控系統內所有物件的外界實體,將其所依賴的物件的引入傳遞(注入)給它。
—— 上面的定義出自Wikipedia
有幾種方式能夠實現控制反轉,下面分別進行展示說明
方法一
// Also frequently called "Container"
class IoC {
/**
* @var PDO The connection to the database
*/
protected $db;
/**
* Create a new instance of Photo and set dependencies.
*/
public static newPhoto()
{
$photo = new Photo;
$photo->setDB(static::$db);
// $photo->setConfig();
// $photo->setResponse();
return $photo;
}
}
$photo = IoC::newPhoto();
現在$photo
變數和我們新建立的Photo
例項具有同樣的效果。這種方法使用者就無需記住和設定所需要的依賴,只需要呼叫newPhoto
方法即可。
方法二
與其為每一個物件建立一個方法,不如寫一個通用的邏輯:
class IoC {
/**
* @var PDO The connection to the database
*/
protected static $registry = array();
/**
* Add a new resolver to the registry array.
* @param string $name The id
* @param object $resolve Closure that creates instance
* @return void
*/
public static function register($name, Closure $resolve)
{
static::$registry[$name] = $resolve;
}
/**
* Create the instance
* @param string $name The id
* @return mixed
*/
public static function resolve($name)
{
if ( static::registered($name) )
{
$name = static::$registry[$name];
return $name();
}
throw new Exception('Nothing registered with that name, fool.');
}
/**
* Determine whether the id is registered
* @param string $name The id
* @return bool Whether to id exists or not
*/
public static function registered($name)
{
return array_key_exists($name, static::$registry);
}
}
不要被這段程式碼嚇到你,這裡的邏輯十分簡單。當使用者呼叫IOC::register
方法時,IoC 僅僅是設定了一個id,例如photo
。和其對應的resolver。resolver的功能是一個匿名函式,其作用是建立例項並設定對應的依賴。
具體我們可以看下面的例子:
// Add `photo` to the registry array, along with a resolver
IoC::register('photo', function() {
$photo = new Photo;
$photo->setDB('...');
$photo->setConfig('...');
return $photo;
});
// Fetch new photo instance with dependencies set
$photo = IoC::resolve('photo');
我們可以看到,我們不是直接例項化一個類,而是透過IoC
容器來進行註冊。
// 之前的寫法
$photo = new Photo;
// 現在的寫法
$photo = IoC::resolve('photo');
擁抱魔術方法
如果你想縮減容器類的程式碼,我們可以利用魔術方法__set()
和__get()
。
class IoC {
protected $registry = array();
public function __set($name, $resolver)
{
$this->registry[$name] = $resolver;
}
public function __get($name)
{
return $this->registry[$name]();
}
}
使用
$c = new IoC;
$c->mailer = function() {
$m = new Mailer;
// create new instance of mailer
// set creds, etc.
return $m;
};
// Fetch, boy
$mailer = $c->mailer; // mailer instance
本作品採用《CC 協議》,轉載必須註明作者和本文連結