軟件構造心得(11):里氏替換原則(Liskov Substitution Principle)保持行爲一致的動機與目的

我們逐漸對於軟件構造有了更深的要求。
設計良好的代碼需要做到可以不通過修改而擴展,新的功能通過添加新的代碼來實現,而不需要更改已有的可工作的代碼。
抽象(Abstraction)和多態(Polymorphism)是實現這一原則的主要機制,而繼承(Inheritance)則是實現抽象和多態的主要方法。而提高這一質量指標,打下構建可維護性和可重用性代碼的基礎,我們首先需要知道一個重要的原則。

本文參考了一些資料和課內內容,將着重解釋,1)什麼是里氏替換原則 2)里氏替換原則保持行爲一致的動機與目的

什麼是里氏替換原則

里氏替換原則(Liskov Substitution Principle),由Barbara Liskov 在 1988 年提出。
在這裏插入圖片描述
一句話概括:就是基類指針可以在任何不知道衍生類的條件下使用衍生類的對象。

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

詳細說明:就是要確保在父類使用的時候,(無論這時候的父類實際上是哪一種衍生類),表現都是一樣的。也就是說,使用者在書寫代碼使用該類時無需關注與考慮有什麼衍生類,以及本次是什麼衍生類。

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

違背LSP思想的一個生動的例子

在運行時查看其所屬於的子類型,我們在之前的學習中就曾經遇見過這種風格的代碼。其顯然破壞了多項原則,其中由於我們需要了解其中的ShapeShape的子類的信息,所以其違背了LSP原則

void DrawShape(const Shape& s)
{
    if (typeid(s) == typeid(Square))
        DrawSquare(static_cast<Square&>(s)); 
    else if (typeid(s) == typeid(Circle))
        DrawCircle(static_cast<Circle&>(s));
}

一個從矩形與正方形的繼承引發的LSP的動機思考

我們來看下面這個面向對象設計中違背LSP原則經典的例子

public class Rectangle
  {
    private double _width;
    private double _height;

    public void SetWidth(double w) { _width = w; }
    public void SetHeight(double w) { _height = w; }
    public double GetWidth() { return _width; }
    public double GetHeight() { return _height; }
  }

這個程序正常工作,沒有問題。我們不妨設想,它被成功地部署到了各個位置,發起作用。然後,假如某一天,我們需要一個正方形類型squaresquare類來處理正方形。
通常來講,繼承關係是一種is-a的關係。換句話來講,如果一種新的對象與一種已有對象滿足 is-a 的關係,那麼新的對象的類應該是從已有對象的類繼承來的。
而這裏面由於很明顯正方形是一個長方形,所以其顯而易見可以從長方形衍生。
然而,當我們什麼也不考慮時,我們可能就會直接寫出如下代碼:

public class Square extends Rectangle
  {
    //Require width == height
  }

進而,我們在使用rectangle的set方法時,如果輸入的其實是square類型變量,那麼該方法就會直接破壞square的RI。

那麼我們單純的避免這種情況,在square中複寫一下的set方法呢?

public class Square extends Rectangle
  {
    public void SetWidth(double w)
    {
      base.SetWidth(w);
      base.SetHeight(w);
    }
    public void SetHeight(double w)
    {
      base.SetWidth(w);
      base.SetHeight(w);
    }
  }

現在,只要設置 Square 對象的 Width,那麼它的 Height 也會相應跟着變化。而當設置其Height 時,Width 也同樣會改變。這樣做之後,Square 仍然可以保持其RI,Square 對象仍然是一個看起來很合理的數學中的正方形,看起來很完美。那麼現在我們擁有了兩個類,無論對於square做什麼,它都保持着對於正方形的定義。對於retangle也是會保持着對於長方形的定義。那麼現在我們可以放心的使用繼承出來的square了嗎?
並沒有…
設想存在下面的情況

void Test1(Rectangle r)
  {
     r.SetWidth(5);
     r.SetHeight(4);
     Assert.AreEqual(r.GetWidth() * r.GetHeight(), 20);
   }

我們可以理解這段代碼的期待。顯然,這個函數做了一個合理的假設,唯獨在我們書寫了square,並且square傳遞進來之後,纔會出現並不符合的問題。因此我們還是不能放心的使用繼承的square。

總結來看,因爲我們要從使用者的合理假設的角度來分析,所以所有的衍生類必須符合使用者所期待的基類的行爲。
那不禁想問,我們已經思考過is-a了啊,那爲什麼既然正方形是長方形,那麼到底哪裏有問題呢?

所以哪錯了?

實際上,到底什麼是is-a,怎麼算做一個is-a。其實,我們的behavioural subtyping概念就此應運而生。其實,我們之前的設計,錯就錯在,一個正方形是一個長方形沒有錯,但是一個square對象並不是一個ractangle對象。因爲我們除了邏輯上的繼承關係,變量上的繼承關係,我們還需要考慮並且滿足子類的行爲繼承關係。

LSP原則其實讓我們意識到了,通過is-a進行繼承的思考與實現是與對象的公有的行爲息息相關的,滿足這個纔算真正實現了is-a。

在每個方法中保證行爲一致性

當我們讓客戶使用基類/接口使用對象,客戶知道的僅僅是其基類的前置條件和後置條件。也就是說其一定只會從這兩個條件做出自己的假設。

那麼我們從這個基類中延展出來的衍生類,就必須要支持基類中的條件,也就是說,其前置條件一定要弱於基類的前置條件,其後置條件一定要強於基類的後置條件。
只有這樣我們纔可以保證基類和衍生類的行爲等價性。放心的重用基類的函數,放心使用或修改衍生類的函數。

LSP:The Liskov Substitution Principle by Robert C. Martin “Uncle Bob”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章