理解依賴注入(Dependency Injection)
我們通過一個示例來解釋為什麼使用使用者服務定位器和依賴注入。首先,假設我們正在開發一個元件,叫SomeComponent。它執行的內容現在還不重要,我們的元件需要依賴資料庫的連線。
在下面第一個例子中,資料庫的連線是在元件內部建立的。這種方法是不實用的;事實上這樣做的話,我們不能改變建立資料庫連線的引數或者選擇不同的資料庫系統,因為連線是當元件被建立時建立的。
class SomeComponent
{
/**
* 連線資料庫的例項是被寫死在元件的內部
* 因此,我們很難從外部替換或者改變它的行為
*/
public function someDbTask ()
{
$connection = new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
// ...
}
}
$some = new SomeComponent ();
$some->someDbTask();
為了解決這樣的情況,我們建立一個setter,在使用前注入獨立外部依賴。現在,看起來似乎是一個不錯的解決辦法:
class SomeComponent
{
protected $_connection;
/**
* 設定外部傳入的資料庫的連線例項
*/
public function setConnection($connection)
{
$this->_connection = $connection ;
}
public function someDbTask()
{
$connection = $this->_connection;
// ...
}
}
$some = new SomeComponent();
// 建立資料庫連線例項
$connection = new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
// 向元件注入資料連線例項
$some->setConnection($connection);
$some->someDbTask();
想一下,假設我們使用這個元件在應用內的好幾個地方都用到,然而我們在注入連線例項時還需要建立好幾次資料的連線例項。 如果我們可以獲取到資料庫的連線例項而不用每次都要建立新的連線例項,使用某種全域性登錄檔可以解決這樣的問題:
class Registry
{
/**
* 返回資料庫連線例項
*/
public static function getConnection()
{
return new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
}
}
class SomeComponent
{
protected $_connection;
/**
* 設定外部傳入的資料庫的連線例項
*/
public function setConnection($connection)
{
$this->_connection = $connection;
}
public function someDbTask()
{
$connection = $this->_connection;
// ...
}
}
$some = new SomeComponent();
// 把登錄檔中的連線例項傳遞給元件
$some->setConnection(Registry::getConnection());
$some->someDbTask();
現在,讓我們設想一下,我們必須實現2個方法,第一個方法是總是建立一個新的連線,第二方法是總是使用一個共享連線:
class Registry
{
protected static $_connection;
/**
* 建立一個新的連線例項
*/
protected static function _createConnection()
{
return new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
}
/**
* 只建立一個連線例項,後面的請求只返回該連線例項
*/
public static function getSharedConnection()
{
if (self::$_connection === null) {
self::$_connection = self::_createConnection();
}
return self::$_connection;
}
/**
* 總是返回一個新的連線例項
*/
public static function getNewConnection()
{
return self::_createConnection();
}
}
class SomeComponent
{
protected $_connection;
/**
* 設定外部傳入的資料庫的連線例項
*/
public function setConnection($connection)
{
$this->_connection = $connection;
}
/**
* 這個方法總是需要共享連線例項
*/
public function someDbTask()
{
$connection = $this->_connection;
// ...
}
/**
* 這個方法總是需要新的連線例項
*/
public function someOtherDbTask($connection)
{
}
}
$some = new SomeComponent();
// 注入共享連線例項
$some->setConnection(
Registry::getSharedConnection()
);
$some->someDbTask();
// 這裡我們總是傳遞一個新的連線例項
$some->someOtherDbTask(
Registry::getNewConnection()
);
到目前為止,我們已經看到依賴注入怎麼解決我們的問題了。把依賴作為引數來傳遞,而不是建立在內部建立它們,這使我們的應用更加容易維護和更加解耦。不管怎麼樣,長期來說,這種形式的依賴注入有一些缺點。
例如,如果這個元件有很多依賴, 我們需要建立多個引數的setter方法來傳遞依賴關係,或者建立一個多個引數的建構函式來傳遞它們,另外在使用元件前還要每次都建立依賴,這讓我們的程式碼像這樣不易維護
// 建立依賴例項或從登錄檔中查詢
$connection = new Connection();
$session = new Session();
$fileSystem = new FileSystem();
$filter = new Filter();
$selector = new Selector();
// 把例項作為引數傳遞給建構函式
$some = new SomeComponent($connection, $session, $fileSystem, $filter, $selector);
// ... 或者使用setter
$some->setConnection($connection);
$some->setSession($session);
$some->setFileSystem($fileSystem);
$some->setFilter($filter);
$some->setSelector($selector);
假設我們必須在應用的不同地方使用和建立這些物件。如果當你永遠不需要任何依賴例項時,你需要去刪掉建構函式的引數,或者去刪掉注入的setter。為了解決這樣的問題,我們再次回到全域性登錄檔建立元件。不管怎麼樣,在建立物件之前,它增加了一個新的抽象層:
class SomeComponent
{
// ...
/**
* Define a factory method to create SomeComponent instances injecting its dependencies
*/
public static function factory()
{
$connection = new Connection();
$session = new Session();
$fileSystem = new FileSystem();
$filter = new Filter();
$selector = new Selector();
return new self($connection, $session, $fileSystem, $filter, $selector);
}
}
瞬間,我們又回到剛剛開始的問題了,我們再次建立依賴例項在元件內部!我們可以繼續前進,找出一個每次能奏效的方法去解決這個問題。但似乎一次又一次,我們又回到了不實用的例子中。
一個實用和優雅的解決方法,是為依賴例項提供一個容器。這個容器擔任全域性的登錄檔,就像我們剛才看到的那樣。使用依賴例項的容器作為一個橋樑來獲取依賴例項,使我們能夠降低我們的元件的複雜性:
use Phalcon\Di;
use Phalcon\DiInterface;
class SomeComponent
{
protected $_di;
public function __construct(DiInterface $di)
{
$this->_di = $di;
}
public function someDbTask()
{
// 獲得資料庫連線例項
// 總是返回一個新的連線
$connection = $this->_di->get("db");
}
public function someOtherDbTask()
{
// 獲得共享連線例項
// 每次請求都返回相同的連線例項
$connection = $this->_di->getShared("db");
// 這個方法也需要一個輸入過濾的依賴服務
$filter = $this->_di->get("filter");
}
}
$di = new Di();
// 在容器中註冊一個db服務
$di->set(
"db",
function () {
return new Connection(
[
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo",
]
);
}
);
// 在容器中註冊一個filter服務
$di->set(
"filter",
function () {
return new Filter();
}
);
// 在容器中註冊一個session服務
$di->set(
"session",
function () {
return new Session();
}
- [ ] List item
);
// 把傳遞服務的容器作為唯一引數傳遞給元件
$some = new SomeComponent($di);
$some->someDbTask();
這個元件現在可以很簡單的獲取到它所需要的服務,服務採用延遲載入的方式,只有在需要使用的時候才初始化,這也節省了伺服器資源。這個元件現在是高度解耦。例如,我們可以替換掉建立連線的方式,它們的行為或它們的任何其他方面,也不會影響該元件。