參考:理解PHP 依賴注入|Laravel IoC容器

2018-02-24 15:51 更新

看Laravel的IoC容器文檔只是介紹實例,但是沒有說原理,之前用MVC框架都沒有在意這個概念,無意中在phalcon的文檔中看到這個詳細的介紹,感覺豁然開朗,復(fù)制粘貼過來,主要是好久沒有寫東西了,現(xiàn)在確實很懶變得!

首先,我們假設(shè),我們要開發(fā)一個組件命名為SomeComponent。這個組件中現(xiàn)在將要注入一個數(shù)據(jù)庫連接。

在這個例子中,數(shù)據(jù)庫連接在component中被創(chuàng)建,這種方法是不切實際的,這樣做的話,我們將不能改變數(shù)據(jù)庫連接參數(shù)及數(shù)據(jù)庫類型等一些參數(shù)。

<?php

class SomeComponent
{

    /**
     * The instantiation of the connection is hardcoded inside
     * the component so is difficult to replace it externally
     * or change its behavior
     */
    public function someDbTask()
    {
        $connection = new Connection(array(
            "host" => "localhost",
            "username" => "root",
            "password" => "secret",
            "dbname" => "invo"
        ));

        // ...
    }

}

$some = new SomeComponent();
$some->someDbTask();

為了解決上面所說的問題,我們需要在使用前創(chuàng)建一個外部連接,并注入到容器中。就目前而言,這看起來是一個很好的解決方案:

<?php

class SomeComponent
{

    protected $_connection;

    /**
     * Sets the connection externally
     */
    public function setConnection($connection)
    {
        $this->_connection = $connection;
    }

    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }

}

$some = new SomeComponent();

//Create the connection
$connection = new Connection(array(
    "host" => "localhost",
    "username" => "root",
    "password" => "secret",
    "dbname" => "invo"
));

//Inject the connection in the component
$some->setConnection($connection);

$some->someDbTask();

現(xiàn)在我們來考慮一個問題,我們在應(yīng)用程序中的不同地方使用此組件,將多次創(chuàng)建數(shù)據(jù)庫連接。使用一種類似全局注冊表的方式,從這獲得一個數(shù)據(jù)庫連接實例,而不是使用一次就創(chuàng)建一次。

<?php

class Registry
{

    /**
     * Returns the connection
     */
    public static function getConnection()
    {
       return new Connection(array(
            "host" => "localhost",
            "username" => "root",
            "password" => "secret",
            "dbname" => "invo"
        ));
    }

}

class SomeComponent
{

    protected $_connection;

    /**
     * Sets the connection externally
     */
    public function setConnection($connection){
        $this->_connection = $connection;
    }

    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }

}

$some = new SomeComponent();

//Pass the connection defined in the registry
$some->setConnection(Registry::getConnection());

$some->someDbTask();

現(xiàn)在,讓我們來想像一下,我們必須在組件中實現(xiàn)兩個方法,首先需要創(chuàng)建一個新的數(shù)據(jù)庫連接,第二個總是獲得一個共享連接:

<?php

class Registry
{

    protected static $_connection;

    /**
     * Creates a connection
     */
    protected static function _createConnection()
    {
        return new Connection(array(
            "host" => "localhost",
            "username" => "root",
            "password" => "secret",
            "dbname" => "invo"
        ));
    }

    /**
     * Creates a connection only once and returns it
     */
    public static function getSharedConnection()
    {
        if (self::$_connection===null){
            $connection = self::_createConnection();
            self::$_connection = $connection;
        }
        return self::$_connection;
    }

    /**
     * Always returns a new connection
     */
    public static function getNewConnection()
    {
        return self::_createConnection();
    }

}

class SomeComponent
{

    protected $_connection;

    /**
     * Sets the connection externally
     */
    public function setConnection($connection){
        $this->_connection = $connection;
    }

    /**
     * This method always needs the shared connection
     */
    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }

    /**
     * This method always needs a new connection
     */
    public function someOtherDbTask($connection)
    {

    }

}

$some = new SomeComponent();

//This injects the shared connection
$some->setConnection(Registry::getSharedConnection());

$some->someDbTask();

//Here, we always pass a new connection as parameter
$some->someOtherDbTask(Registry::getConnection());

到此為止,我們已經(jīng)看到了如何使用依賴注入解決我們的問題。不是在代碼內(nèi)部創(chuàng)建依賴關(guān)系,而是讓其作為一個參數(shù)傳遞,這使得我們的程序更容易維護,降低程序代碼的耦合度,實現(xiàn)一種松耦合。但是從長遠來看,這種形式的依賴注入也有一些缺點。

例如,如果組件中有較多的依賴關(guān)系,我們需要創(chuàng)建多個setter方法傳遞,或創(chuàng)建構(gòu)造函數(shù)進行傳遞。另外,每次使用組件時,都需要創(chuàng)建依賴組件,使代碼維護不太易,我們編寫的代碼可能像這樣:

<?php

//Create the dependencies or retrieve them from the registry
$connection = new Connection();
$session = new Session();
$fileSystem = new FileSystem();
$filter = new Filter();
$selector = new Selector();

//Pass them as constructor parameters
$some = new SomeComponent($connection, $session, $fileSystem, $filter, $selector);

// ... or using setters

$some->setConnection($connection);
$some->setSession($session);
$some->setFileSystem($fileSystem);
$some->setFilter($filter);
$some->setSelector($selector);

我想,我們不得不在應(yīng)用程序的許多地方創(chuàng)建這個對象。如果你不需要依賴的組件后,我們又要去代碼注入部分移除構(gòu)造函數(shù)中的參數(shù)或者是setter方法。為了解決這個問題,我們再次返回去使用一個全局注冊表來創(chuàng)建組件。但是,在創(chuàng)建對象之前,它增加了一個新的抽象層:

<?php

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);
    }

}

這一刻,我們好像回到了問題的開始,我們正在創(chuàng)建組件內(nèi)部的依賴,我們每次都在修改以及找尋一種解決問題的辦法,但這都不是很好的做法。

一種實用和優(yōu)雅的來解決這些問題,是使用容器的依賴注入,像我們在前面看到的,容器作為全局注冊表,使用容器的依賴注入做為一種橋梁來解決依賴可以使我們的代碼耦合度更低,很好的降低了組件的復(fù)雜性:

<?php

class SomeComponent
{

    protected $_di;

    public function __construct($di)
    {
        $this->_di = $di;
    }

    public function someDbTask()
    {

        // Get the connection service
        // Always returns a new connection
        $connection = $this->_di->get('db');

    }

    public function someOtherDbTask()
    {

        // Get a shared connection service,
        // this will return the same connection everytime
        $connection = $this->_di->getShared('db');

        //This method also requires a input filtering service
        $filter = $this->_db->get('filter');

    }

}

$di = new Phalcon\DI();

//Register a "db" service in the container
$di->set('db', function(){
    return new Connection(array(
        "host" => "localhost",
        "username" => "root",
        "password" => "secret",
        "dbname" => "invo"
    ));
});

//Register a "filter" service in the container
$di->set('filter', function(){
    return new Filter();
});

//Register a "session" service in the container
$di->set('session', function(){
    return new Session();
});

//Pass the service container as unique parameter
$some = new SomeComponent($di);

$some->someTask();

現(xiàn)在,該組件只有訪問某種service的時候才需要它,如果它不需要,它甚至不初始化,以節(jié)約資源。該組件是高度解耦。他們的行為,或者說他們的任何其他方面都不會影響到組件本身。
我們的實現(xiàn)辦法?

Phalcon\DI 是一個實現(xiàn)了服務(wù)的依賴注入功能的組件,它本身也是一個容器。

由于Phalcon高度解耦,Phalcon\DI 是框架用來集成其他組件的必不可少的部分,開發(fā)人員也可以使用這個組件依賴注入和管理應(yīng)用程序中不同類文件的實例。

基本上,這個組件實現(xiàn)了 Inversion of Control 模式?;诖耍瑢ο蟛辉僖詷?gòu)造函數(shù)接收參數(shù)或者使用setter的方式來實現(xiàn)注入,而是直接請求服務(wù)的依賴注入。這就大大降低了整體程序的復(fù)雜性,因為只有一個方法用以獲得所需要的一個組件的依賴關(guān)系。

此外,這種模式增強了代碼的可測試性,從而使它不容易出錯。
在容器中注冊服務(wù)?

框架本身或開發(fā)人員都可以注冊服務(wù)。當一個組件A要求調(diào)用組件B(或它的類的一個實例),可以從容器中請求調(diào)用組件B,而不是創(chuàng)建組件B的一個實例。

這種工作方式為我們提供了許多優(yōu)點:

我們可以更換一個組件,從他們本身或者第三方輕松創(chuàng)建。
在組件發(fā)布之前,我們可以充分的控制對象的初始化,并對對象進行各種設(shè)置。
我們可以使用統(tǒng)一的方式從組件得到一個結(jié)構(gòu)化的全局實例

服務(wù)可以通過以下幾種方式注入到容器:

<?php

//Create the Dependency Injector Container
$di = new Phalcon\DI();

//By its class name
$di->set("request", 'Phalcon\Http\Request');

//Using an anonymous function, the instance will lazy loaded
$di->set("request", function(){
    return new Phalcon\Http\Request();
});

//Registering directly an instance
$di->set("request", new Phalcon\Http\Request());

//Using an array definition
$di->set("request", array(
    "className" => 'Phalcon\Http\Request'
));

在上面的例子中,當向框架請求訪問一個請求數(shù)據(jù)時,它將首先確定容器中是否存在這個”reqeust”名稱的服務(wù)。

容器會反回一個請求數(shù)據(jù)的實例,開發(fā)人員最終得到他們想要的組件。

在上面示例中的每一種方法都有優(yōu)缺點,具體使用哪一種,由開發(fā)過程中的特定場景來決定的。

用一個字符串來設(shè)定一個服務(wù)非常簡單,但缺少靈活性。設(shè)置服務(wù)時,使用數(shù)組則提供了更多的靈活性,而且可以使用較復(fù)雜的代碼。lambda函數(shù)是兩者之間一個很好的平衡,但也可能導(dǎo)致更多的維護管理成本。

Phalcon\DI 提供服務(wù)的延遲加載。除非開發(fā)人員在注入服務(wù)的時候直接實例化一個對象,然后存存儲到容器中。在容器中,通過數(shù)組,字符串等方式存儲的服務(wù)都將被延遲加載,即只有在請求對象的時候才被初始化。

<?php

//Register a service "db" with a class name and its parameters
$di->set("db", array(
    "className" => "Phalcon\Db\Adapter\Pdo\Mysql",
    "parameters" => array(
          "parameter" => array(
               "host" => "localhost",
               "username" => "root",
               "password" => "secret",
               "dbname" => "blog"
          )
    )
));

//Using an anonymous function
$di->set("db", function(){
    return new Phalcon\Db\Adapter\Pdo\Mysql(array(
         "host" => "localhost",
         "username" => "root",
         "password" => "secret",
         "dbname" => "blog"
    ));
});

以上這兩種服務(wù)的注冊方式產(chǎn)生相同的結(jié)果。然后,通過數(shù)組定義的,在后面需要的時候,你可以修改服務(wù)參數(shù):

<?php

$di->setParameter("db", 0, array(
    "host" => "localhost",
    "username" => "root",
    "password" => "secret"
));

從容器中獲得服務(wù)的最簡單方式就是使用”get”方法,它將從容器中返回一個新的實例:

<?php $request = $di->get("request");

或者通過下面這種魔術(shù)方法的形式調(diào)用:

<?php

$request = $di->getRequest();
//Phalcon\DI 同時允許服務(wù)重用,為了得到一個已經(jīng)實例化過的服務(wù),可以使用 getShared() 方法的形式來獲得服務(wù)。

具體的 Phalcon\Http\Request 請求示例:

    <?php
    $request = $di->getShared("request");

參數(shù)還可以在請求的時候通過將一個數(shù)組參數(shù)傳遞給構(gòu)造函數(shù)的方式:

<?php
    $component = $di->get("MyComponent", array("some-parameter", "other"));

原文地址:http://www.yuansir-web.com/?p=1012&preview=true

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號