深刻理解IoC和DI

本文章轉載自: https://segmentfault.com/a/1190000005602011php


最近在研究php的lumen框架和phalcon框架,這兩個框架的底層架構都用到了IoC,DI,關於這兩個概念本身一直沒理解更清晰,找到一篇寫得很是好的博文,在此作個備份記錄。程序員

基本概念


依賴倒置原則(DIP)(Dependency Inversion Principle):一種軟件架構設計的原則(抽象概念)。數據庫

控制反轉(IoC)(Inversion of Control):一種反轉流、依賴和接口的方式(DIP的具體實現方式)。segmentfault

依賴注入(DI)(Dependency Injection):IoC的一種實現方式,用來反轉依賴(IoC的具體實現方式)。設計模式

IoC容器 :依賴注入的框架,用來映射依賴,管理對象建立和生存週期(DI框架)。服務器

依賴倒置原則


依賴倒置原則,它轉換了依賴,高層模塊不依賴於低層模塊的實現,而低層模塊依賴於高層模塊定義的接口。通俗的講,就是高層模塊定義接口,低層模塊負責實現。session

Bob Martins對DIP的定義:
高層模塊不該依賴於低層模塊,二者應該依賴於抽象。
抽象不該該依賴於實現,實現應該依賴於抽象。架構

場景一 依賴無倒置(低層模塊定義接口,高層模塊負責實現)


從上圖中,咱們發現高層模塊的類依賴於低層模塊的接口。所以,低層模塊須要考慮到全部的接口。若是有新的低層模塊類出現時,高層模塊須要修改代碼,來實現新的低層模塊的接口。這樣,就破壞了開放封閉原則。框架

場景二 依賴倒置(高層模塊定義接口,低層模塊負責實現)

在這個圖中,咱們發現高層模塊定義了接口,將再也不直接依賴於低層模塊,低層模塊負責實現高層模塊定義的接口。這樣,當有新的低層模塊實現時,不須要修改高層模塊的代碼。函數

由此,咱們能夠總結出使用DIP的優勢:

系統更柔韌:能夠修改一部分代碼而不影響其餘模塊。

系統更健壯:能夠修改一部分代碼而不會讓系統崩潰。

系統更高效:組件鬆耦合,且可複用,提升開發效率。

控制反轉 (IoC)


DIP是一種 軟件設計原則,它僅僅告訴你兩個模塊之間應該如何依賴,可是它並無告訴如何作。IoC則是一種 軟件設計模式,它告訴你應該如何作,來解除相互依賴模塊的耦合。控制反轉(IoC),它爲相互依賴的組件提供抽象,將依賴(低層模塊)對象的得到交給第三方(系統)來控制,即依賴對象不在被依賴模塊的類中直接經過new來獲取。

軟件設計原則:原則爲咱們提供指南,它告訴咱們什麼是對的,什麼是錯的。它不會告訴咱們如何解決問題。它僅僅給出一些準則,以便咱們能夠設計好的軟件,避免不良的設計。一些常見的原則,好比DRY、OCP、DIP等。

軟件設計模式:模式是在軟件開發過程當中總結得出的一些可重用的解決方案,它能解決一些實際的問題。一些常見的模式,好比工廠模式、單例模式等等。

IoC有2種常見的實現方式:依賴注入和服務定位。主要的實現方式依賴注入。

依賴注入 (DI)


控制反轉(IoC)一種重要的方式,就是將依賴對象的建立和綁定轉移到被依賴對象類的外部來實現。

依賴注入(DI),它提供一種機制,將須要依賴(低層模塊)對象的引用傳遞給被依賴(高層模塊)對象。

php中傳遞依賴的方法

方法一 直接在高層模塊new 底層模塊的類

class Bim
    {
        public function doSomething()
        {
            echo __METHOD__, '|';
        }
    }
    
    class Bar
    {
        public function doSomething()
        {
            $bim = new Bim();
            $bim->doSomething();
            echo __METHOD__, '|';
        }
    }
    
    class Foo
    {
        public function doSomething()
        {
            $bar = new Bar();
            $bar->doSomething();
            echo __METHOD__;
        }
    }
    
    $foo = new Foo();
    $foo->doSomething(); //Bim::doSomething|Bar::doSomething|Foo::doSomething

缺點:類與類之間的耦合程度過高,Foo必須依賴Bar,沒有Bar,Foo就不能工做,這種方案最不可取。

方法二 構造函數注入

構造函數函數注入,毫無疑問經過構造函數傳遞依賴。所以,構造函數的參數必然用來接收一個依賴對象。那麼參數的類型是什麼呢?具體依賴對象的類型?仍是一個抽象類型?根據DIP原則,咱們知道高層模塊不該該依賴於低層模塊,二者應該依賴於抽象。那麼構造函數的參數應該是一個抽象類型。

class Bim
    {
        public function doSomething()
        {
            echo __METHOD__, '|';
        }
    }
    
    class Bar
    {
        private $bim;
    
        public function __construct(Bim $bim)
        {
            $this->bim = $bim;
        }
    
        public function doSomething()
        {
            $this->bim->doSomething();
            echo __METHOD__, '|';
        }
    }
    
    class Foo
    {
        private $bar;
    
        public function __construct(Bar $bar)
        {
            $this->bar = $bar;
        }
    
        public function doSomething()
        {
            $this->bar->doSomething();
            echo __METHOD__;
        }
    }
    
    $foo = new Foo(new Bar(new Bim()));
    $foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething

缺點:若是依賴過多,那麼在構造方法裏必然傳入多個參數,三個以上就會使代碼變的難以閱讀。

方法三 setter注入

相比構造函數注入,setter注入顯得有些複雜,使用也不常見。具體思路是先定義一個接口,包含一個設置依賴的方法。而後依賴類,繼承並實現這個接口。

/**
 * 接口
 */
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

缺點:一樣存在和第二種方案同樣的弊端,當依賴的類增多時,咱們須要些不少不少的set方法。

基於以上的缺點,這時咱們在想若是有一個專門的類(或者說一個容器)能夠幫咱們管理這些依賴關係就行了。

方法四 IoC容器注入

對於大型項目來講,相互依賴的組件比較多。若是還用手動的方式,本身來建立和注入依賴的話,顯然效率很低,並且每每還會出現不可控的場面。正因如此,IoC容器誕生了。IoC容器其實是一個DI框架,它能簡化咱們的工做量。它包含如下幾個功能:

  • 管理應用程序中的『全局』對象(包括 動態建立、注入依賴對象、管理對象生命週期、映射依賴關係)
  • 能夠延時加載對象(僅用到時才建立對象)
  • 促進編寫可重用、可測試和鬆耦合的代碼
class SomeComponent
{

    protected $_di;

    public function __construct($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 Phalcon\DI();

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

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

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

//把傳遞服務的容器做爲惟一參數傳遞給組件
$some = new SomeComponent($di);
$some->someTask();

這個組件如今能夠很簡單的獲取到它所須要的服務,服務採用延遲加載的方式(由於註冊的時候,都是傳入了一個匿名函數,只有在須要使用的時候才初始化),這也節省了服務器資源。這個組件如今是高度解耦。例如,咱們能夠替換掉建立鏈接的方式,它們的行爲或它們的任何其餘方面,也不會影響該組件。

參考資料

原則&模式|理解DIP、IoC、DI以及IoC容器
PHP程序員如何理解IoC/DI
PHP程序員如何理解依賴注入容器(dependency injection container)
Laravel 依賴注入思想
php依賴注入

相關文章
相關標籤/搜索