PHP程序員如何理解IoC/DI

思想

思想是解決問題的根本思想必須轉(zhuǎn)換成習(xí)慣構(gòu)建一套完整的思想體系是開發(fā)能力成熟的標志——《簡單之美》(前言)
“成功的軟件項目就是那些提交產(chǎn)物達到或超出客戶的預(yù)期的項目,而且開發(fā)過程符合時間和費用上的要求,結(jié)果在面對變化和調(diào)整時有彈性。”——《面向?qū)ο蠓治雠c設(shè)計》(第3版)P.236

術(shù)語介紹

——引用《Spring 2.0 技術(shù)手冊》林信良

非侵入性 No intrusive

框架的目標之一是非侵入性(No intrusive)

組件可以直接拿到另一個應(yīng)用或框架之中使用

增加組件的可重用性(Reusability)

容器(Container)

管理對象的生成、資源取得、銷毀等生命周期

建立對象與對象之間的依賴關(guān)系

啟動容器后,所有對象直接取用,不用編寫任何一行代碼來產(chǎn)生對象,或是建立對象之間的依賴關(guān)系。

IoC

控制反轉(zhuǎn) Inversion of Control

依賴關(guān)系的轉(zhuǎn)移

依賴抽象而非實踐

DI

依賴注入 Dependency Injection

不必自己在代碼中維護對象的依賴

容器自動根據(jù)配置,將依賴注入指定對象

AOP

Aspect-oriented programming

面向方面編程

無需修改任何一行程序代碼,將功能加入至原先的應(yīng)用程序中,也可以在不修改任何程序的情況下移除。

分層

表現(xiàn)層:提供服務(wù),顯示信息。領(lǐng)域?qū)樱哼壿?,系統(tǒng)中真正的核心。數(shù)據(jù)源層:與數(shù)據(jù)庫、消息系統(tǒng)、事務(wù)管理器及其它軟件包通信?!镀髽I(yè)應(yīng)用架構(gòu)模式》P.14

代碼演示IoC

假設(shè)應(yīng)用程序有儲存需求,若直接在高層的應(yīng)用程序中調(diào)用低層模塊API,導(dǎo)致應(yīng)用程序?qū)Φ蛯幽K產(chǎn)生依賴。

/**
 * 高層
 */
class Business
{
    private $writer;

    public function __construct()
    {
        $this->writer = new FloppyWriter();
    }

    public function save()
    {
        $this->writer->saveToFloppy();
    }
}

/**
 * 低層,軟盤存儲
 */
class FloppyWriter
{
    public function saveToFloppy()
    {
        echo __METHOD__;
    }
}

$biz = new Business();
$biz->save(); // FloppyWriter::saveToFloppy

假設(shè)程序要移植到另一個平臺,而該平臺使用USB磁盤作為存儲介質(zhì),則這個程序無法直接重用,必須加以修改才行。本例由于低層變化導(dǎo)致高層也跟著變化,不好的設(shè)計。
正如前方提到的

控制反轉(zhuǎn) Inversion of Control
依賴關(guān)系的轉(zhuǎn)移
依賴抽象而非實踐

程序不應(yīng)該依賴于具體的實現(xiàn),而是要依賴抽像的接口。請看代碼演示

/**
 * 接口
 */
interface IDeviceWriter
{
    public function saveToDevice();
}

/**
 * 高層
 */
class Business
{
    /**
     * @var IDeviceWriter
     */
    private $writer;

    /**
     * @param IDeviceWriter $writer
     */
    public function setWriter($writer)
    {
        $this->writer = $writer;
    }

    public function save()
    {
        $this->writer->saveToDevice();
    }
}

/**
 * 低層,軟盤存儲
 */
class FloppyWriter implements IDeviceWriter
{

    public function saveToDevice()
    {
        echo __METHOD__;
    }
}

/**
 * 低層,USB盤存儲
 */
class UsbDiskWriter implements IDeviceWriter
{

    public function saveToDevice()
    {
        echo __METHOD__;
    }
}

$biz = new Business();
$biz->setWriter(new UsbDiskWriter());
$biz->save(); // UsbDiskWriter::saveToDevice

$biz->setWriter(new FloppyWriter());
$biz->save(); // FloppyWriter::saveToDevice

控制權(quán)從實際的FloppyWriter轉(zhuǎn)移到了抽象的IDeviceWriter接口上,讓Business依賴于IDeviceWriter接口,且FloppyWriter、UsbDiskWriter也依賴于IDeviceWriter接口。
這就是IoC,面對變化,高層不用修改一行代碼,不再依賴低層,而是依賴注入,這就引出了DI。
比較實用的注入方式有三種:

1.Setter injection 使用setter方法
2.Constructor injection 使用構(gòu)造函數(shù)
3.Property Injection 直接設(shè)置屬性

事實上不管有多少種方法,都是IoC思想的實現(xiàn)而已,上面的代碼演示的是Setter方式的注入。

依賴注入容器 Dependency Injection Container

1.管理應(yīng)用程序中的『全局』對象(包括實例化、處理依賴關(guān)系)。

2.可以延時加載對象(僅用到時才創(chuàng)建對象)。

3.促進編寫可重用、可測試和松耦合的代碼。

理解了IoC和DI之后,就引發(fā)了另一個問題,引用Phalcon文檔描述如下:
如果這個組件有很多依賴, 我們需要創(chuàng)建多個參數(shù)的setter方法??來傳遞依賴關(guān)系,或者建立一個多個參數(shù)的構(gòu)造函數(shù)來傳遞它們,另外在使用組件前還要每次都創(chuàng)建依賴,這讓我們的代碼像這樣不易維護

//創(chuàng)建依賴實例或從注冊表中查找
$connection = new Connection();
$session = new Session();
$fileSystem = new FileSystem();
$filter = new Filter();
$selector = new Selector();

//把實例作為參數(shù)傳遞給構(gòu)造函數(shù)
$some = new SomeComponent($connection, $session, $fileSystem, $filter, $selector);

// ... 或者使用setter

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

假設(shè)我們必須在應(yīng)用的不同地方使用和創(chuàng)建這些對象。如果當你永遠不需要任何依賴實例時,你需要去刪掉構(gòu)造函數(shù)的參數(shù),或者去刪掉注入的setter。為了解決這樣的問題,我們再次回到全局注冊表創(chuàng)建組件。不管怎么樣,在創(chuàng)建對象之前,它增加了一個新的抽象層:

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)部!我們可以繼續(xù)前進,找出一個每次能奏效的方法去解決這個問題。但似乎一次又一次,我們又回到了不實用的例子中。
一個實用和優(yōu)雅的解決方法,是為依賴實例提供一個容器。這個容器擔任全局的注冊表,就像我們剛才看到的那樣。使用依賴實例的容器作為一個橋梁來獲取依賴實例,使我們能夠降低我們的組件的復(fù)雜性:

class SomeComponent
{

    protected $_di;

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

    public function someDbTask()
    {

        // 獲得數(shù)據(jù)庫連接實例
        // 總是返回一個新的連接
        $connection = $this->_di->get('db');

    }

    public function someOtherDbTask()
    {

        // 獲得共享連接實例
        // 每次請求都返回相同的連接實例
        $connection = $this->_di->getShared('db');

        // 這個方法也需要一個輸入過濾的依賴服務(wù)
        $filter = $this->_di->get('filter');

    }

}

$di = new Phalcon\DI();

//在容器中注冊一個db服務(wù)
$di->set('db', function() {
    return new Connection(array(
        "host" => "localhost",
        "username" => "root",
        "password" => "secret",
        "dbname" => "invo"
    ));
});

//在容器中注冊一個filter服務(wù)
$di->set('filter', function() {
    return new Filter();
});

//在容器中注冊一個session服務(wù)
$di->set('session', function() {
    return new Session();
});

//把傳遞服務(wù)的容器作為唯一參數(shù)傳遞給組件
$some = new SomeComponent($di);

$some->someTask();

這個組件現(xiàn)在可以很簡單的獲取到它所需要的服務(wù),服務(wù)采用延遲加載的方式,只有在需要使用的時候才初始化,這也節(jié)省了服務(wù)器資源。這個組件現(xiàn)在是高度解耦。例如,我們可以替換掉創(chuàng)建連接的方式,它們的行為或它們的任何其他方面,也不會影響該組件。

參考文章

PHP程序員如何理解依賴注入容器(dependency injection container)

http://docs.phalconphp.com/zh/latest/reference/di.html

What is Dependency Injection? Fabien Potencier

Inversion of Control Containers and the Dependency Injection pattern by Martin Fowler

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容