fbpx

淺談 IoC/DI 依賴注射與控制反轉

dependencytheory
http://hist140.wikia.com/wiki/Dependency_Theory

IoC/DI 分別是 Inversion of Control (控制反轉) 與 Dependency Injection (依賴注射) 這兩個單字的合體,字面上看起來實在是有點繞口。簡單的說,IoC (Inversion of Control) 控制反轉是一種物件導向程式設計的概念,主要的目的也是為降低系統耦合度,將程式中的控制邏輯進行移轉,讓程式碼之間的控制關係簡化。IoC 這個詞聽起來實在是太玄了,其中最常見的實作技巧就是 DI (Dependency Injection) 依賴注射設計模式 (Design Pattern),可以讓你的程式架構進行解耦合,更容易實現可以動態組合的模組化架構。

物件導向程式設計 (OOD) 可以說是程式設計進步的一個里程碑,有了物件導向,大幅提高了程式碼的維護性,一併讓程式碼的重用性也更高了。但是相反的,物件導向也帶來類別與類別之間的依賴關係,很久以前我有寫過一篇「這個物件導向有點依賴!」文章,其中提到一些系統耦合性的概念。這個物件依賴關係假想敵,隨著系統的複雜性提高,程式間的依賴關係也隨之提高,當系統大到一定的程度時,依賴關係將變得難以控制。通常到這個局面程式碼的耦合性已經很,即使透過程式已經透過介面 (Interface) 進行隔離,也朝著模組化進行設計,但是為了實體化眾多介面的實作類別 (簡單來說就是 new Class) 好讓程式執行,難免會讓系統變成一個不容易即時抽換的假模組化架構,造成抽換模組還是需要動手改程式碼,一點也不靈活。

依賴注射典型解耦合設計模式

DI (Dependency Injection) 依賴注射比起 IoC 應該會好理解很多,主要是實現「抽離類別實體化」行為的一種設計模式,說實在的聽起來可能還是很抽象。那舉個例子說明好了,當我們的程式碼用到 new 這樣的動作時,這份程式碼就會相依這個 Class,即使已經透過介面進行設計,在實體化的過程中還是免不了類別(與介面)的依賴,如下程式範例:

<?php

/**
 * Class Zoo
 */
class Zoo {

    /**
     * @var Interface_Animal
     */
    private $_smallAnimal = null;

    /**
     * @var Interface_Animal
     */
    private $_largeAnimal = null;

    public function __constructor()
    {
        $this->_smallAnimal = new Cat();
        $this->_largeAnimal = new Horse();
    }

}

上述的程式碼可以看出 Zoo Class 相依 Interface_Animal 介面,然而在 Zoo Class 建構子 (constructor) 中,為了實體化 Interface_Animal 的實作,寫下了 new Cat() 與 new Horse(),這個動作同時也依賴了 Cat 與 Horse 這兩個類別,程式間的耦合性變的更高了。如果我們展開所有類別與介面,依賴類別給「兩分」,依賴介面給「一分」,展開的結果如下:

iocdi-grid-1

把上述的分數加起來後除以類別 (與介面) 的總數,得到 7/4 = 1.75 分,這可以大概表示整個系統的耦合程度,數字越大越差,但這不是什麼正規的評估方法,在這裡僅僅是個用來比較的相對值。

Getter/Setter 解耦技巧

繼續上面的例子,如果我們改寫一下 Zoo Class 透過 getter/setter 來注入 (Inject) 實體,將這個實體化動作轉移到外部的第三者來進行,同時也將這個類別相依的鳥事交給另一個衰人來處理,如下:

<?php

/**
 * Class Zoo
 */
class Zoo {

    /**
     * @var Interface_Animal
     */
    private $_smallAnimal = null;

    /**
     * @var Interface_Animal
     */
    private $_largeAnimal = null;

    public function __constructor()
    {
        // nothing to do
    }

    public function setSmallAnimal(Interface_Animal &$animal)
    {
        $this->_smallAnimal = $animal;
    }

    public function setLargeAnimal(Interface_Animal &$animal)
    {
        $this->_largeAnimal = $animal;
    }
}

然後建立一個 Container Class 來主導整個 Runtime 的物件注射工作,如下:

<?php

/**
 * Class Container
 */
class Container {
    
    public function __constructor()
    {
        $zoo = new Zoo();
        $zoo->setSmallAnimal(new Cat());
        $zoo->setLargeAnimal(new Horse());
    }
    
}

這樣一來 Zoo Class 就不再相依 Cat 與 Dog Class 了,重新整理一下相依性的表格如下:

iocdi-grid-2

計算之後的分數為 9/5 = 1.8 變高了,但是如果不考慮 Container (未來利用其他技巧進行強制解耦),這樣分數就變成 3/5 = 0.6,就差很多了,這個透過 getter/setter 的設計模式解耦是非常傳統的方法,如果考慮 Container Class 能做的解耦程度有限,接下來我們談談其他方法。(上述所有程式碼在 GitHub)

Dependency Injection 依賴注射

想一下剛剛的例子,一但 Container 需要實體化更多類別,這個被委託的衰類別豈不是更衰了,滿滿的相依性與耦合關係都聚集在這裡,就像葡萄一樣一整串。但不要緊,程式設計的藝術家們可是很有創造力的,我們可以透過 Reflection (反射)、Annotation (注譯) 等等技巧,把這些類別間的依賴關係昇華為另一種超越既有程式語言的表現模式,像是透過 XML, JSON, 注譯等等方式重新描述物件之間的使用關係,把類別之間原本在 Compile Time 發生的相依關係,轉移到 Run Time 來進行。這樣的技巧在 Java 的世界裡已經行之有年,Java 的開發者應該都對 Spring 這樣的 Framework 有些認識,Spring 算是對 IoC/DI 概念實作相當完善的案例,為了達到更深入的 IoC 實現,Spring 甚至透過 Java Bytecode 來植入框架與呼叫流程,算是非常高階的技巧。

反觀 PHP 這樣廣泛被使用在 Web 的程式語言,IoC/DI 就不那麼常被使用,也可能是因為典型的 PHP 架構都是閱過即焚的特性,不像 Java, Node.JS 有 Container Runtime 可以持續實體化資源,反觀 PHP 在複雜的控制流程、模組化與 Runtime 架構在一般系統中比較少見。我們下次就來介紹一下用 PHP 實現 IoC/DI 的做法,下次見囉.......

有興趣可以繼續約讀這一篇文章「用 PHP 實現 IoC/DI (控制反轉與依賴注射) 設計模式

發佈留言