1. 程式人生 > >PHP程式設計師如何理解IoC/DI

PHP程式設計師如何理解IoC/DI

思想

思想是解決問題的根本
思想必須轉換成習慣
構建一套完整的思想體系是開發能力成熟的標誌
——《簡單之美》(前言)

.

“成功的軟體專案就是那些提交產物達到或超出客戶的預期的專案,而且開發過程符合時間和費用上的要求,結果在面對變化和調整時有彈性。”
——《面向物件分析與設計》(第3版)P.236

術語介紹

——引用《Spring 2.0 技術手冊》林信良

非侵入性 No intrusive

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

  • 元件可以直接拿到另一個應用或框架之中使用

  • 增加元件的可重用性(Reusability)

容器(Container)

  • 管理物件的生成、資源取得、銷燬等生命週期

  • 建立物件與物件之間的依賴關係

  • 啟動容器後,所有物件直接取用,不用編寫任何一行程式碼來產生物件,或是建立物件之間的依賴關係。

IoC

  • 控制反轉 Inversion of Control

  • 依賴關係的轉移

  • 依賴抽象而非實踐

DI

  • 依賴注入 Dependency Injection

  • 不必自己在程式碼中維護物件的依賴

  • 容器自動根據配置,將依賴注入指定物件

AOP

  • Aspect-oriented programming

  • 面向方面程式設計

  • 無需修改任何一行程式程式碼,將功能加入至原先的應用程式中,也可以在不修改任何程式的情況下移除。

分層

表現層:提供服務,顯示資訊。
領域層:邏輯,系統中真正的核心。
資料來源層:與資料庫、訊息系統、事務管理器及其它軟體包通訊。
——《企業應用架構模式》P.14

程式碼演示IoC

假設應用程式有儲存需求,若直接在高層的應用程式中呼叫低層模組API,導致應用程式對低層模組產生依賴。

/** * 高層 */ 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

假設程式要移植到另一個平臺,而該平臺使用USB磁碟作為儲存介質,則這個程式無法直接重用,必須加以修改才行。本例由於低層變化導致高層也跟著變化,不好的設計。

正如前方提到的

控制反轉 Inversion of Control
依賴關係的轉移
依賴抽象而非實踐

程式不應該依賴於具體的實現,而是要依賴抽像的介面。請看程式碼演示

/** * 介面 */ 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

控制權從實際的FloppyWriter轉移到了抽象的IDeviceWriter介面上,讓Business依賴於IDeviceWriter介面,且FloppyWriter、UsbDiskWriter也依賴於IDeviceWriter介面。

這就是IoC,面對變化,高層不用修改一行程式碼,不再依賴低層,而是依賴注入,這就引出了DI。

比較實用的注入方式有三種:

  • Setter injection 使用setter方法

  • Constructor injection 使用建構函式

  • Property Injection 直接設定屬性

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

依賴注入容器 Dependency Injection Container

  • 管理應用程式中的『全域性』物件(包括例項化、處理依賴關係)。

  • 可以延時載入物件(僅用到時才建立物件)。

  • 促進編寫可重用、可測試和鬆耦合的程式碼。

理解了IoC和DI之後,就引發了另一個問題,引用Phalcon文件描述如下:

如果這個元件有很多依賴, 我們需要建立多個引數的setter方法​​來傳遞依賴關係,或者建立一個多個引數的建構函式來傳遞它們,另外在使用元件前還要每次都建立依賴,這讓我們的程式碼像這樣不易維護

//建立依賴例項或從登錄檔中查詢 $connection = new Connection(); $session = new Session(); $fileSystem = new FileSystem(); $filter = new Filter(); $selector = new Selector(); //把例項作為引數傳遞給建構函式 $some = new SomeComponent($connection, $session, $fileSystem, $filter, $selector); // ... 或者使用setter $some->setConnection($connection); $some->setSession($session); $some->setFileSystem($fileSystem); $some->setFilter($filter); $some->setSelector($selector);

假設我們必須在應用的不同地方使用和建立這些物件。如果當你永遠不需要任何依賴例項時,你需要去刪掉建構函式的引數,或者去刪掉注入的setter。為了解決這樣的問題,我們再次回到全域性登錄檔建立元件。不管怎麼樣,在建立物件之前,它增加了一個新的抽象層:

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

瞬間,我們又回到剛剛開始的問題了,我們再次建立依賴例項在元件內部!我們可以繼續前進,找出一個每次能奏效的方法去解決這個問題。但似乎一次又一次,我們又回到了不實用的例子中。

一個實用和優雅的解決方法,是為依賴例項提供一個容器。這個容器擔任全域性的登錄檔,就像我們剛才看到的那樣。使用依賴例項的容器作為一個橋樑來獲取依賴例項,使我們能夠降低我們的元件的複雜性:

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

這個元件現在可以很簡單的獲取到它所需要的服務,服務採用延遲載入的方式,只有在需要使用的時候才初始化,這也節省了伺服器資源。這個元件現在是高度解耦。例如,我們可以替換掉建立連線的方式,它們的行為或它們的任何其他方面,也不會影響該元件。

參考文章

補充

很多程式碼背後,都是某種哲學思想的體現。

以下引用《面向模式的軟體架構》卷1模式系統第六章模式與軟體架構

軟體架構支援技術(開發軟體時要遵循的基本原則)

  1. 抽象

  2. 封裝

  3. 資訊隱藏

  4. 分離關注點

  5. 耦合與內聚

  6. 充分、完整、簡單

  7. 策略與實現分離

    • 策略元件負責上下文相關決策,解讀資訊的語義和含義,將眾多不同結果合併或選擇引數值

    • 實現元件負責執行定義完整的演算法,不需要作出與上下文相關的決策。上下文和解釋是外部的,通常由傳遞給元件的引數提供。

  8. 介面與實現分離

    • 介面部分定義了元件提供的功能以及如何使用該元件。元件的客戶端可以訪問該介面。

    • 實現部分包含實現元件提供的功能的實際程式碼,還可能包含僅供元件內部使用的函式和資料結構。元件的客戶端不能訪問其實現部分。

  9. 單個引用點

    • 軟體系統中的任何元素都應只宣告和定義一次,避免不一致性問題。
      10. 分而治之

軟體架構的非功能特性

  1. 可修改性

    • 可維護性

    • 可擴充套件性

    • 重組

    • 可移植性

  2. 互操作性

    • 與其它系統或環境互動

  3. 效率

  4. 可靠性

    • 容錯:發生錯誤時確保行為正確並自行修復

    • 健壯性:對應用程式進行保護,抵禦錯誤的使用方式和無效輸入,確保發生意外錯誤時處於指定狀態。

  5. 可測試性

  6. 可重用性

    • 通過重用開發軟體

    • 開發軟體時考慮重用