PHP OO 物件導向原則:里氏替換原則LSP


OO_Principle_LSP

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

 里氏替換原則 LSP (Liskov Substitution Principle)

一、定義與說明:

所有子類別都可以代理父類別的工作。
Subtypes must be substitutable for their base types.
里氏替換原則原則要能夠成立,介面(interface)/抽象方法(Abstract method) 就必須要遵守定義去實做。
又被稱為 Design by Contract ,即按照契約設計 ,子類別需兌現對父類別的承諾,遵照父類別設計開發。
作為子類別的方法必須和他們父類別的方法操作一致,子類別中可以擁有父類別沒有的特殊功能,但是繼承的方法,功能應該兩者一致的。
子類不只是實現父類別的方法,而且必須名符其實,否則會發生無法預料的事情。

二、遵守單一職責原則的結果:

確保行為的正確性:
Client 端只會透過介面與抽象方法的定義來判斷一個方法的行為,如果有遵守 LSP 原則,就可以確保動作的行為會如預期般的運作。
如果程式沒有達到 LSP 原則,程式的行為將變得「不可預測」,換句話說可能產生不可預知且不容易察覺的 bugs。
簡單的例子是:
如果有一個父類別的 method 名稱叫做 getName,但是子類別實作 getName 的時候卻回傳 $this->id,這樣的情況下,別人看到 getName 的 function 就以為他是回傳 Name ,但實際上是回傳 ID ,而且該失誤無法在建置時發現,可能需要執行時才會發生錯誤。

三、如何設計里氏替換原則的類別?:

子類別必須遵從父類別或介面的設計理念去實作方法

經典錯誤範例:

在程式設計中,正方形是矩形的子類別嗎?
Rectangle 矩形:
class Rectangle {

    private $topLeft;
    private $width;
    private $height;

    public function setHeight($height) {
        $this->height = $height;
    }

    public function getHeight() {
        return $this->height;
    }

    public function setWidth($width) {
        $this->width = $width;
    }

    public function getWidth() {
        return $this->width;
    }
}
這是一個矩形的類別,含有 width 和 height 屬性,以及 setter 與 getter 。想像我們的應用程式已經開發完成並且有多個 Client 已經開始使用了,現在這些 Client 要求要有新功能:操控正方形。
在現實生活與幾何學中,正方形是長方形的一部分。所以我們建立了一個 Square 類別來繼承 Rectangle 類別。通常來說,繼承關系是 is-a 的關系,就如同“正方形 is a 矩形”。很明顯一個正方形是一個長方形,可以滿足所有常規的目的和用途。這就建立了 is-a 的關系,Square 的類別可以從 Rectangle 衍生。
但是在程式設計中,正方形真的是矩形的一種嗎?
Square 正方形:
class Square extends Rectangle {

    public function setHeight($value) {
        $this->width = $value;
        $this->height = $value;
    }

    public function setWidth($value) {
        $this->width = $value;
        $this->height = $value;
    }
}
依照”正方形是矩形的一種”來設計,正方形也有 width 與 heigth,只是他們是等長的。因此我們可以覆寫父類別的 setter 來讓 width 跟 heigth 相等。
接下來要來驗證正方形會如何影響 Client 的程式..
class Client {

    function areaVerifier(Rectangle $r) {
        $r->setWidth(5);
        $r->setHeight(4);

        if($r->area() != 20) {
            throw new Exception('Bad area!');
        }

        return true;
    }

}
由上方程式碼可得知這是一個計算矩形面積的 client 程式,並且會在程式運行結果不如預期的時候丟出例外訊息。
function area() {
    return $this->width * $this->height;
}
當然我們需要在 Rectangle 矩形類別中加入一個 method area() 來提供 client 需要的面積。
驗證:
最後我們開始驗證程式,傳遞一個空的 rectangle 物件與空的 Square 物件至 areaVerifier() 方法:
    function testRectangleArea() {
        $r = new Rectangle();
        $c = new Client();
        $c->areaVerifier($r);
    }
function testSquareArea() {
    $r = new Square();
    $c = new Client();
    $c->areaVerifier($r);
}
其中 areaVerifier()的類型提示TypeHint類型為 Rectangle 類別。
如果 Square 正方形類別有繼承 Rectangle 類別的話,那麼areaVerifier()方法的類型提示TypeHint並不會出現錯誤。但是真正運行的結果卻是錯誤的!輸出結果:
Exception : Bad area!
#0 /paht/: /.../.../LspTest.php(18): Client->areaVerifier(Object(Square))
#1 [internal function]: LspTest->testSquareArea()

結論:

為什麼要這樣驗證呢?因為依照 LSP 原則來說“子類別一定可以取代父類別”,但是 Square 覆寫了父類別的方法後卻沒辦法提供與 Ractangle 一樣的結果,也就是違反了”子類別一定可以取代父類別”的條件。
開放封閉原則(Open Closed Principle)是許多物件導向的啟示思想的核心。符合該原則的應用程序在可維護性、可重用性和彈性等方面會表現的更好。
里氏替換原則(Liskov Substitution Principle)則是實現 OCP 原則的重要方式。只有當子類別能夠完全替代它們的父類別類時,使用父類別的函數才能夠被安全的重用,然后子生類也可以被放心的修改了。

資源參考:


留言

這個網誌中的熱門文章

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

PHP OO 物件導向基礎教學

Gitlab 合併請求 Merge Request 是什麼?