理解PHP依賴注入容器(dependency injection container)系列(一) 什麼是依賴注入
本文是PHP依賴注入容器的實現這個系列的第一章。
今天,先不談容器(container),首先用一些具體的例子來介紹依賴注入的概念,證明依賴注入這種模式可以解決哪些問題,同時能給開發人員帶來哪些好處。
如果你已經知道了依賴注入的概念,你可以跳過這篇文章。
依賴注入可能是我所知道的最簡單設計模式之一,很多情況下可能你無意識中已經使用了依賴注入。不過它也是最難解釋的一個。我認為有一部分原因是由於大多數介紹依賴注入的例子缺乏實際意義,讓人難理解。因為PHP主要用於Web開發,那就先來看一個簡單的web開發例項。
HTTP本身是一個無狀態的連線協議,為了支援客戶在發起WEB請求時應用程式能儲存使用者資訊,我們就需要通過一種技術來實現儲存狀態互動。理所當然最簡單的是使用cookie,更好的方式是PHP內建的Session機制。
$_SESSION['language'] = 'fr';
上面程式碼將使用者語言儲存在了名為language的Session變數中,因此在該使用者隨後的請求中,可以通過全域性陣列$_SESSION來獲取language:
$user_language = $_SESSION['language'];
依賴注入主要用於面向對像開發,現在讓我們假設我們有一個SessionStorage類,該類封裝了PHP Session機制:
class SessionStorage
{
function __construct($cookieName = 'PHP_SESS_ID')
{
session_name($cookieName );
session_start();
}
function set($key, $value)
{
$_SESSION[$key] = $value;
}
function get($key)
{
return $_SESSION[$key];
}
// ...
}
同時還有一個User類提供了更高階的封裝:
class User
{
protected $storage;
function __construct()
{
$this->storage = new SessionStorage();
}
function setLanguage($language)
{
$this->storage->set('language', $language);
}
function getLanguage()
{
return $this->storage->get('language');
}
// ...
}
程式碼很簡單,同樣使用User類也很簡單:
$user = new User();
$user->setLanguage('fr');
$user_language = $user->getLanguage();
一切都很美好,除非你的程式需要更好的擴充套件性。假設現在你想要更改儲存session_id的COOKIE鍵值,以下有一些可供選擇的方法:
- User類中建立SessionStorage例項時,在SessionStorage構造方法中使用字串’SESSION_ID’硬編碼:
class User
{
function __construct()
{
$this->storage = new SessionStorage('SESSION_ID');
}
// ...
}
- 在User類外部設定一個常量(名為STORAGE_SESSION_NAME)
class User
{
function __construct()
{
$this->storage = new SessionStorage(STORAGE_SESSION_NAME);
}
// ...
}
define('STORAGE_SESSION_NAME', 'SESSION_ID');
- 通過User類建構函式中的引數傳遞Session name
class User
{
function __construct($sessionName)
{
$this->storage = new SessionStorage($sessionName);
}
// ...
}
$user = new User('SESSION_ID');
- 還是通過User類建構函式中的引數傳遞Session name,不過這次引數採用陣列的方式
class User
{
function __construct($storageOptions)
{
$this->storage = new SessionStorage($storageOptions['session_name']);
}
// ...
}
$user = new User(array('session_name' => 'SESSION_ID'));
上面的方式都很糟糕。
在user類中硬編碼設定session name的做法沒有真正解決問題,如果以後你還需要更改儲存session_id的COOKIE鍵值,你不得不再一次修改user類(User類不應該關心COOKIE鍵值)。
使用常量的方式同樣很糟,造成User類依賴於一個常量設定。
通過User類建構函式的引數或陣列來傳遞session name相對來說好一些,不過也不完美,這樣做干擾了User類建構函式的引數,因為如何儲存Session並不是User類需要關心的,User類不應該和它們扯上關聯。
另外,還有一個問題不太好解決:我們如何改變SessionStorage類。這種應用場景很多,比如你要用一個Session模擬類來做測試,或者你要將Session儲存在資料庫或者記憶體中。目前這種實現方式,在不改變User類的情況下,很難做到這點。
現在,讓我們來使用依賴注入。回憶一下,之前我們是在User類內部建立SessionStorage對像的,現在我們修改一下,讓SessionStorage對像通過User類的建構函式傳遞進去。
class User
{
function __construct($storage)
{
$this->storage = $storage;
}
// ...
}
這就是依賴注入最經典的案例,沒有之一。現在使用User類有一些小小的改變,首先你需要建立SessionStorage對像。
$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);
現在,配置session儲存對像很簡單了,同樣如果改變session儲存對像也很簡單,所有這一切並不需要去更新User類,降低了業務類之間的耦合。
Pico Container 的網站上是這樣描述依賴注入:
依賴注入是通過類的建構函式、方法、或者直接寫入的方式,將所依賴的元件傳遞給類的方式。
所以依賴注入並不只限於通過建構函式注入。下面來看看幾種注入方式:
- 建構函式注入
class User
{
function __construct($storage)
{
$this->storage = $storage;
}
// ...
}
- setter方法注入
class User
{
function setSessionStorage($storage)
{
$this->storage = $storage;
}
// ...
}
- 屬性直接注入
class User
{
public $sessionStorage;
}
$user->sessionStorage = $storage;
根據經驗,一般通過建構函式注入的是強依賴關係的元件,setter方式用來注入可選的依賴元件。
現在,大多數流行的PHP框架都採用了依賴注入的模式實現業務元件間的高內聚低耦合。
// symfony: 建構函式注入的例子
$dispatcher = new sfEventDispatcher();
$storage = new sfMySQLSessionStorage(array('database' => 'session', 'db_table' => 'session'));
$user = new sfUser($dispatcher, $storage, array('default_culture' => 'en'));
// Zend Framework: setter方式注入的例子
$transport = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth' => 'login',
'username' => 'foo',
'password' => 'bar',
'ssl' => 'ssl',
'port' => 465,
));
$mailer = new Zend_Mail();
$mailer->setDefaultTransport($transport);
如果對依賴注入有興趣,強烈推薦你看《Martin Fowler introduction》或者著名的《Jeff More presentation》
這就是本章的全部內容,希望對大家在理解依賴注入上有所幫助。在該系列後面的內容中,我們將討論依賴注入的容器實現。