PHP OO 物件導向原則:依賴反轉原則DIP


OO_Principle_DIP

物件導向有五個原則 S.O.L.I.D. :
  • 單一職責原則 SRP (Single Responsibility Principle)
  • 開放封閉原則 OCP (Open Closed Principle)
  • 里氏替換原則 LSP (Liskov Substitution Principle)
  • 介面隔離原則 ISP (Interface Segregation Principle)
  • 依賴反轉原則 DIP (Dependency Inversion Principle)
更多物件導向的理論的學習內容會整理到 PHP OO 的進階教學:

 依賴反轉原則 DIP (Dependency Inversion Principle)

一、定義與說明:

定義:
  1. 高階模組不應該依賴於低階模組,兩者都該依賴抽象。
    High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. 抽象不應該由低階模組定義。
    Abstractions should not depend on details.
  3. 低階模組的實作內容應該依照抽象的定義去打造。
    Details should depend on abstractions.
高階與低階,是相對關係,其實也就是 呼叫者 (Caller)被呼叫者 (Callee)
越高階的模組接近商業邏輯(電子商務買賣交易流程等),越低階的模組越接近實作邏輯(讀寫資料庫的、計算金額邏輯等)
接下來用範例來講解有沒有使用依賴反轉的差異吧!

範例:

如果你有遵守前幾項原則的話,要使用依賴反轉(DIP)就會變得很簡單唷!
現在,我們來做一個電子書 e-book 閱讀器的應用程式:
class Test extends PHPUnit_Framework_TestCase {

    function testItCanReadAPDFBook() {
        $b = new PDFBook();
        $r = new PDFReader($b);

        $this->assertRegExp('/pdf book/', $r->read());
    }

}

class PDFReader {

    private $book;

    function __construct(PDFBook $book) {
        $this->book = $book;
    }

    function read() {
        return $this->book->read();
    }
}

class PDFBook {

    function read() {
        return "reading a pdf book.";
    }
}
在一開始,我們使用 PDF 閱讀器來開發電子書閱讀器,到目前為止,我們有一個使用 PDFBook 的 PDFReader 類別。PDF閱讀器的 read() 方法對應 PDFBook 的 read 方法。
目前兩個類別的關係為:

使用一個 PDF 閱讀器來閱讀 PDB 書本的話,以上的程式碼算是很完整了,但是我們的目標是一個可以支持多種格式的電子書閱讀器,因此我們重新命名一下閱讀器類別的名稱: PDFReader ⇒ EBookReader
class Test extends PHPUnit_Framework_TestCase {

    function testItCanReadAPDFBook() {
        $b = new PDFBook();
        $r = new EBookReader($b);

        $this->assertRegExp('/pdf book/', $r->read());
    }
}

class EBookReader {

    private $book;

    function __construct(PDFBook $book) {
        $this->book = $book;
    }

    function read() {
        return $this->book->read();
    }
}

class PDFBook {

    function read() {
        return "reading a pdf book.";
    }
}
以上,只重新命名,把 PDFReader 改成 EBookReader,其他的程式都沒有更動,所以程式測試結果仍然是可以正常運行的:
Testing started at 1:04 PM ...
PHPUnit 3.7.28 by Sebastian Bergmann.
Time: 13 ms, Memory: 2.50Mb
OK (1 test, 1 assertion)
Process finished with exit code 0
但是這樣設計會有一個大問題:

「高階模組」直接依賴「低階模組」
 如果 EBookReader 依賴於這個 PDFBook 低階模組的話,表示 EBookReader 已經被 PDF 類型綁死了,
除了 PDF 檔案類型,EBookReader 不再能夠讀取其他檔案囉。因此不應該讓任何東西直接依賴低階模組
為了改善 EBookReader 被綁死的問題,我們依照 EBookReader 需要讀取書本的需求,來訂製一個 EBook 介面(interface):
class Test extends PHPUnit_Framework_TestCase {

    function testItCanReadAPDFBook() {
        $b = new PDFBook();
        $r = new EBookReader($b);

        $this->assertRegExp('/pdf book/', $r->read());
    }
}

interface EBook {
    function read();
}

class EBookReader {

    private $book;

    function __construct(EBook $book) {
        $this->book = $book;
    }

    function read() {
        return $this->book->read();
    }
}

class PDFBook implements EBook{

    function read() {
        return "reading a pdf book.";
    }
}
這個範例是由「高階模組」定義抽象介面讓「低階模組」去實作。
把一個抽象層引入到程式裡面,是依賴翻轉的一個常見的手段,這時候「高階模組」與「低階模組」都能夠依賴於介面上,
以上程式碼是一個遵循介面隔離原則的程式,由 Client 端程式的需求來製定介面的內容。
照目前的設計來看,就是一個電子書閱讀器 EBookReader 使用了電子書 EBook 的閱讀` read() 方法。
如上圖,現在的程式從一個依賴變成兩個依賴
  • 第一個依賴是 EBookReader指向 EBook 介面,把介面當作一種使用的方式, EBookReader 使用 EBooks。
  • 第二個依賴是 PDFBook 指向 EBook 介面,但卻是用來實作介面。
    PDFBook 只是眾多電子書 EBook 的其中一種,因此PDFBook實作介面來滿足客戶端 EBookReader 的需求。
這個解決方案還允許我們在電子書閱讀器中新增不同類型的電子書,只要這些書的種類符合電子書 EBook 的定義,那麼就可以透過實作EBook介面來滿足EBookReader的需求:
class Test extends PHPUnit_Framework_TestCase {

    function testItCanReadAPDFBook() {
        $b = new PDFBook();
        $r = new EBookReader($b);

        $this->assertRegExp('/pdf book/', $r->read());
    }

    function testItCanReadAMobiBook() {
        $b = new MobiBook();
        $r = new EBookReader($b);

        $this->assertRegExp('/mobi book/', $r->read());
    }
}

interface EBook {
    function read();
}

class EBookReader {

    private $book;

    function __construct(EBook $book) {
        $this->book = $book;
    }

    function read() {
        return $this->book->read();
    }
}

class PDFBook implements EBook {

    function read() {
        return "reading a pdf book.";
    }
}

class MobiBook implements EBook {

    function read() {
        return "reading a mobi book.";
    }
}
這也滿足了 OCP 開放封閉原則,開放擴增,封閉修改的原則。在不更動原程式碼的情況下,擴增新的需求。

結論:

依賴反轉原則可以幫助我們遵守其他原則,遵守依賴反轉的過程中會:
  • 讓你很容易達成 OCP 開放封閉原則
  • 拆散類別的職責(SRP)
  • 拆散介面的職責(ISP)
  • 避免父子類別沒有依照介面的定義實作(LSP)

資料參考


留言

這個網誌中的熱門文章

Git Commit Message 這樣寫會更好,替專案引入規範與範例

PHP OO 物件導向基礎教學

Gitlab 合併請求 Merge Request 是什麼?