最近更新: 2010-08-15

PHP 實作 IoC/DI 設計模式

當我們將許多個體組合為一個新的個體時,按一般的個體導向程式設計模式,我們會在新個體的型態定義內部明確地描述其組合元件的來源。而控制反轉(IoC) 又稱依賴注入(Dependency Injection)模式,則將組合元件的來源從定義內部挪到定義外部。在靜態型別程式語言中, IoC 設計模式有相當重要的地位。在 Java 世界中重要的 Spring framework 就是因為採用 IoC 為核心設計模式,才突破 Java 語言的僵固狀態,為那些 Java 教士帶來新的福音。不過在動態型別程式語言中,實現 IoC 設計模式倒是一件簡單的事,我們不會特別注意它,甚至不會想到原來自己用了 IoC 設計模式。

日前公司同事找我討論在 PHP 實作 IoC 模式的問題。因為他之前寫 Java 程式,接觸 Spring framework 後,覺得那實在是很棒的東西,也想用 PHP 寫一套出來用。我對他說,PHP 要實作 IoC 不會很難,但是你不會感受到 Java 加上 Spring framework 之後生產力突飛猛進的雀躍感。在討論過程中,我順手用 PHP 寫了一段採用屬性注入(setter injection)的 IoC 類別與範例。實作時間不到二小時。這個 IoC 類別基本上模仿了 Spring framework ,所以本文說明時的用語,將會借用 Spring framework 的用語。

範例所用的類別

在開始實作 IoC 類別之前,我先寫兩個很簡單的類別 MyClass1, MyClass2,就像 Java 的 POJO 一樣很普通的類別。

<?php
// lib.php
// Somebody designs those classes.
class MyClass1 {
    function getValue() {
        return 1;
    }
}

class MyClass2 {
    function getValue() {
        return '2';
    }
}
?>

接著,假設我要用 MyClass1 與 MyClass2 組合一個新的類別 MyClass3 。在一般的設計模式中,我可能會寫出下列的程式碼。

<?php
class MyClass3 {
    var $c1;
    var $c2;

    function __construct() {
        //你需要在類別內指出 c1,c2 是怎麼來的。
        $this->c1 = new MyClass1();
        $this->c2 = new MyClass2();
    }

    function dump() {
        echo $this->c1->getValue(), "\n";
        echo $this->c2->getValue(), "\n";
    }
}
?>

上列程式碼的建構行為中,明確地描述了屬性 c1, c2 之來源為新建的 MyClass1, MyClass2 實體。這就是在新個體與其依賴元件之間的耦合。而 IoC 則要將這個控制行為反轉,移到外部進行。

按照 IoC 設計模式改寫 MyClass3 ,結果為下列程式碼。

<?php
// MyClass3.php
//You design this class which contains two items, but you don't need to know
//what those items exactly are.
class MyClass3 {
    /*
    @Autowired
    MyClass1 c1;
    */
    static $Autowired = array(
            'myClass1', // 為 bean 名稱,且令屬性為同名。
            'a1' => array('ref' => 'myClass1')
        );
    var $c1;
    var $c2;

    function dump() {
        echo $this->c1->getValue(), "\n";
        echo $this->c2->getValue(), "\n";
    }
}
?>

改寫後的 MyClass3 程式碼,最明顯的差異就是配置 c1, c2 的程式碼消失了。因為我們把它移到外部了。我們現在不必知道那兩個屬性實際上到底是什麼。至於 $Autowired 則是我待會要實作的另一種屬性注入方式所需的關鍵。

實作 IoC 類別

配合 IoC 設計模式,我實作了一個 BeanFactory 類別。它只實現了屬性注入能力,可以做到以下三件事:

  • 根據組態文件,建立 bean 與 class 的對照關係。我實作的這個 IoC 模式中,屬性的依賴對象是 bean ,而不是實際的類別。
  • 根據 bean 的組態內容,在配置 bean 時,為它的屬性注入依賴的 bean 實體。
  • 根據類別定義的 $Autowired 內容(若存在時),注入依賴的 bean 實體。
<?php
// BeanFactory.php
class BeanFactory {
    static $context = False;

    function LoadContext($context) {
        self::$context = $context;
    }

    // New is keyword, using Alloc to instead.
    function Alloc($beanName) {
        if ( !self::$context or !isset(self::$context[$beanName]['class']))
            throw new ErrorException("Bean '$beanName' not found.");
        $beanContext = &self::$context[$beanName];

        $class = $beanContext['class'];
        $instance = new $class();
        if (isset($beanContext['property'])
          and is_array($beanContext['property']))
        {
            foreach ($beanContext['property'] as $name => $value) {
                $instance->$name = self::Wire($value);
            }
        }
        if (property_exists($class, 'Autowired')) {
            //foreach ($class::$Autowired as $name => $value) {
            //PHP 5.3 才支援上一列的寫法,PHP 5.2 要用下兩列。
            $class_vars = get_class_vars($class);
            foreach ($class_vars['Autowired'] as $name => $value) {
                if (is_int($name))
                    // $value 即為 bean 名稱,且令屬性為同名。
                    $instance->$value = self::Alloc($value);
                else
                    $instance->$name = self::Wire($value);
            }
        }
        return $instance;
    }

    function Wire(&$value) {
        if (is_array($value) and isset($value['ref'])) {
            return self::Alloc($value['ref']);
        }
        return $value;
    }
}
?>
組織 bean 與類別的關係

下列內容定義了 bean 與類別的關係,這份內容將會提供給 BeanFactory 使用。其組織方式模仿了 Spring framework ,請參考《Introduction to the Spring IoC container and beans》。

<?php
// bean-config
/*
<beans>
    <bean id="myClass1" class="MyClass1">
    </bean>
    <bean id="myClass2" class="MyClass2">
    </bean>
    <bean id="myClass3" class="MyClass3">
        <property name="c1"><ref bean="myClass1"/></property>
        <property name="c2"><ref bean="myClass2"/></property>
        <property name="c3"><value>hello</value></property>
    </bean>
</beans>
*/
$myContext = array(
    'myClass1' => array(
        'class' => 'MyClass1'
        ),
    'myClass2' => array(
        'class' => 'MyClass2'
        ),
    'myClass3' => array(
        'class' => 'MyClass3',
        'property' => array(
            'c1' => array('ref' => 'myClass1'),
            'c2' => array('ref' => 'myClass2'),
            'c3' => 'hello'
            )
        )
    );
?>

實務上,我們會選擇將此設定內容以 JSON 表達與儲存。使用時再從文件內讀取。 如果你想虛擲歲月,你也可以採用 Spring 的XML格式。

應用 IoC 的範例
應用一

以下是一個應用上述實作的 IoC 模式的範例,它將透過 BeanFactory 配置一個 myClass3 bean (按組態文件,它關聯到 MyClass3 類別) 的實體,傾印它的內部結構,呼叫它的 dump() 及其屬性的 getValue()。

<?php
// main.php
require_once 'lib.php';
function __autoload($className) {
    require_once $className . '.php';
    //echo "load ", $className, "\n";
}
require_once 'BeanFactory.php';

require_once 'bean-config.ini';
//$myContext = json_decode(file_get_contents('bean-config.json'));
BeanFactory::LoadContext($myContext);

$obj = BeanFactory::Alloc('myClass3');

var_dump($obj);

$obj->dump();
echo $obj->c3, "\n";

// Autowired
echo '$obj->a1->getValue(): ', $obj->a1->getValue(), "\n";
echo '$obj->myClass1->getValue(): ', $obj->myClass1->getValue(), "\n";
?>

執行結果如下所示。BeanFactory 確實根據組態文件的內容,將 c1, c2兩個屬性注入指定的 bean 實體;根據 $Autowired 的指示,注入 myClass1, a1 兩個屬性。

object(MyClass3)#1 (5) {
  ["c1"]=>
  object(MyClass1)#2 (0) {
  }
  ["c2"]=>
  object(MyClass2)#3 (0) {
  }
  ["c3"]=>
  string(5) "hello"
  ["myClass1"]=>
  object(MyClass1)#4 (0) {
  }
  ["a1"]=>
  object(MyClass1)#5 (0) {
  }
}
1
2
hello

有經驗的程序人員將注意到,在 MyClass3 中完全不知道 c1, c2 實際上是什麼東西,所以從程式碼來看,不能保證這兩個屬性具有 getValue() 方法。 實務上,我們有兩條路可以確保 c1, c2 兩個屬性都擁有這裡用到的行為:

  1. 寫一個 test case.
  2. 在 BeanFactory 類處理的 $context 之中添加更多可用資訊,例如 指定 bean 的介面,然後自己用反射機制檢查。

從實務經驗來看,既然我們並不是在用 Java 語言,便沒有必要把 Java 語言學到的壞習慣帶過來,因此第一條路比較簡單而且可靠。

對於錯誤捕捉之事。靜態語言仰賴各種宣告資訊,提供 compiler 在編譯時期核對。但我因為以往有 C 語言實作的經驗,所以向來不太倚重 interface 防錯能力。

另一方面,動態語言沒有編譯時期,它們在執行時期才能確定個體資訊,故必須仰賴更動態的處理策略。現行最有效的策略就是玩真的,提供 test case 供 tester 在執行時期測試。

為什麼我在寫動態語言程式時,不像寫 C/C++ 那麼強調型態宣告,卻不覺得程式會出錯?後來想到了,那是因為有 TDD。而且 tester 所產生的結果更可靠。

個體之間協議互動行為的多種形式
應用二

第二個應用,我將新增一個 MyClass22

<?php
// MyClass22.php
class MyClass22 extends MyClass2 {
    function getValue() {
        return 'ext' . parent::getValue();
    }
}
?>

修改組態文件 bean-config.ini ,將 myClass2 bean 的關聯類別改為 MyClass22。其他的程式碼,包含 main.php 都不更動任何地方。

<?php
// bean-config
$myContext = array(
    'myClass1' => array(
        'class' => 'MyClass1'
        ),
    'myClass2' => array(
        'class' => 'MyClass22' //我修改了這個bean實際關聯的類別為 MyClass22
        ),
    'myClass3' => array(
        'class' => 'MyClass3',
        'property' => array(
            'c1' => array('ref' => 'myClass1'),
            'c2' => array('ref' => 'myClass2'),
                //屬性的依賴對象是 bean,而不是類別,所以這裡不動。
            'c3' => 'hello'
            )
        )
    );
?>

在其他程式碼不變的情形下,再次執行 main.php 的結果如下:

object(MyClass3)#1 (5) {
  ["c1"]=>
  object(MyClass1)#2 (0) {
  }
  ["c2"]=>
  object(MyClass22)#3 (0) {
  }
  ["c3"]=>
  string(5) "hello"
  ["myClass1"]=>
  object(MyClass1)#4 (0) {
  }
  ["a1"]=>
  object(MyClass1)#5 (0) {
  }
}
1
ext2
hello

myClass2 bean 關聯的類別改變了,而原本由 c2 的 getValue() 輸出的結果也從 2 變為 ext2 。

在沒有 IoC 的狀況下,要達成上述結果,你得要回頭修改 MyClass3 的程式碼,將 c2 的實體從 MyClass2 改成 MyClass22 。如果你有十個其他類別用到 MyClass2 ,而且你也要改用 MyClass22 的話,你還要修改那十個類別的程式碼。 藉由 IoC 設計模式,你將原先散佈在其他類別內的依賴元件的配置動作,集中在同一個地方控制(在此例中,由 BeanFactory 根據 bean 的組態文件控制),就不必為了改變依賴關係,而修改原有的程式碼。依賴注入模式將產生依賴元件的責任,從新個體反轉到外部,實現「控制反轉」,降低程式碼的耦合度。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/13454601.html

樂多舊回應
ycheng.tw@gmail.com(Y.C Cheng) (#comment-21096223)
Fri, 20 Aug 2010 12:02:19 +0800
IoC是否需要以及是否會變成主流, 跟該語言特性以及藉由該特性所衍生出該語言使用者的品味有關.

php容易實做IoC是一回事, 但是為何php演化中不會把IoC演化進去, 是另一個有趣的議題.
未留名 (#comment-21129391)
Mon, 23 Aug 2010 17:07:28 +0800
PHP 可以隨意地指派(或增加)一個實體中的屬性。但同樣的事,由 Java 來做卻要大費周張。這就是為何 Spring framework 花了大把時間做的事,在本文的實作中卻只花了50行程式碼。

而且,PHP 有許多種不同的設計模式可以規劃屬性注入的方式。除本文特地模仿 Spring framework 的方式之外,我亦曾實作過其他注入模式,但都沒說這是 IoC 。因為我覺得沒必要特別強調。

另一方面,支援 mixin 能力的程式語言,如 Ruby ,藉由 mixin 所能發揮的設計彈性更大於 IoC 。那些程式語言的使用者就更少談論 IoC 。

因此對於用 PHP 實作 IoC 這件事,我個人不置可否。因為那麼做並不會讓我們的生產力突飛猛進,不算是一件值得投注精力的工作。當作益智遊戲就好了。