設計模式-----里氏替換原則

里氏替換原則

開放封閉原則(Open Closed Principle)是構建可維護性和可重用性代碼的基礎。它強調設計良好的代碼能夠不經過修改而擴展,新的功能經過添加新的代碼來實現,而不須要更改已有的可工做的代碼。抽象(Abstraction)和多態(Polymorphism)是實現這一原則的主要機制,而繼承(Inheritance)則是實現抽象和多態的主要方法。html

那麼是什麼設計規則在保證對繼承的使用呢?優秀的繼承層級設計都有哪些特徵呢?是什麼在誘使咱們構建了不符合開放封閉原則的層級結構呢?這些就是本篇文章將要回答的問題。程序員

里氏替換原則(LSP: The Liskov Substitution Principle)編程

使用基類對象指針或引用的函數必須可以在不瞭解衍生類的條件下使用衍生類的對象。安全

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

Barbara Liskov 在 1988 年提出了這一原則:ide

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 原則的一個簡單示例post

一個很是明顯地違背 LSP原則的示例就是使用 RTTI(Run Time Type Identification)來根據對象類型選擇函數執行。.net

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

顯然 DrawShape 函數的設計存在不少問題。它必須知道全部 Shape 基類的衍生子類,而且當有新的子類被建立時就必須修改這個函數。事實上,不少人看到這個函數的結構都認爲是在詛咒面向對象設計。設計

正方形和長方形,違背原則的微妙之處

不少狀況下對 LSP 原則的違背方式都十分微妙。設想在一個應用程序中使用了 Rectangle 類,描述以下:

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

試想這個應用程序能夠良好地工做,而且已被部署到了多個位置。就像全部成功的軟件同樣,它的用戶提了新的需求。假設某一天用戶要求該應用程序除了可以處理長方形(Rectangle)以外還要可以處理正方形(Square)。

一般來講,繼承關係是 is-a 的關係。換句話講,若是一種新的對象與一種已有對象知足 is-a 的關係,那麼新的對象的類應該是從已有對象的類繼承來的。

很明顯一個正方形是一個長方形,能夠知足全部常規的目的和用途。所以這就創建了 is-a 的關係,Square 的邏輯模型能夠從 Rectangle 衍生。

對 is-a 關係的使用是面向對象分析(Object Oriented Analysis)的基本技術之一。一個正方形是一個(is-a)長方形,全部 Square 類應當從 Rectangle 類衍生。然而這種思考方式將引發一些微妙的卻很嚴重的問題。一般在咱們沒有實際使用這些代碼以前,這些問題是沒法被預見的。

關於這個問題,咱們的第一個線索多是Square 類並不須要 _height 和 _width 成員變量,儘管不管如何它都繼承了它們。能夠看出這是一種浪費,並且若是咱們持續建立成百上千個 Square 對象,這種浪費就會表現的十分明顯。

儘管如此,咱們也能夠假設咱們並非十分關心內存的開銷。那還有什麼問題嗎?固然!Square 類將繼承 SetWidth 和 SetHeight 方法。這些方法對於 Square 來講是徹底不適當的,由於一個正方形的長和寬是同樣的。這就應該是另外一個顯著的線索了。然而,有一種方法能夠規避這個問題。咱們能夠覆寫SetWidth 和 SetHeight 方法。以下所示:

public class Square : 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 看起來很完美了。Square 對象仍然是一個看起來很合理的數學中的正方形。

public void TestCase1()
{
    Square s = new Square();
    s.SetWidth(1); // Fortunately sets the height to 1 too.
    s.SetHeight(2); // sets width and heigt to 2, good thing.
}

但如今看下下面這個方法:

void f(Rectangle r)
{
    r.SetWidth(32); // calls Rectangle::SetWidth
}

若是咱們傳遞一個 Square 對象的引用到這個方法中,則 Square 對象將被損壞,由於它的 Height 將不會被更改。這裏明確地違背了 LSP 原則,此函數在衍生對象爲參數的條件下沒法正常工做。而失敗的緣由是由於在父類 Rectangle 中沒有將 SetWidth 和 SetHeight 設置爲 virtual 函數。

咱們也能很容易的解決這個問題。但儘管這樣,當建立一個衍生類將致使對父類作出修改,一般意味着這個設計是有缺陷的,具體的說就是它違背了 OCP 原則。咱們可能會認爲真正的設計瑕疵是忘記了將SetWidth 和 SetHeight 設置爲 virtual 函數,並且咱們已經修正了這個問題。可是,其實也很難自圓其說,由於設置 Rectangle 的 Height 和 Width 已經再也不是一個原子操做。不管是何種緣由咱們將它們設置爲 virtual,咱們都將沒法預期 Square 的存在。

還有,假設咱們接收了這個參數,而且解決了這些問題。咱們最終獲得了下面這段代碼:

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

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

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

問題的根源

此時此刻咱們有了兩個類,Square 和 Rectangle,並且看起來能夠工做。不管你對 Square 作什麼,它仍能夠保持與數學中的正方形定義一致。並且也無論你對 Rectangle 對象作什麼,它也將符合數學中長方形的定義。而且當你傳遞一個 Square 對象到一個能夠接收 Rectangle 指針或引用的函數中時,Square 仍然能夠保證正方形的一致性。

既然這樣,咱們可能得出結論了,這個模型如今是自洽的(self-consistent)和正確的。可是,這個結論實際上是錯誤的。一個自洽的模型不必定對它的全部用戶都保持一致!

(注:自洽性即邏輯自洽性和概念、觀點等的先後一向性。首先是指建構一個科學理論的若干個基本假設之間,基本假設和由這些基本假設邏輯地導出的一系列結論之間,各個結論之間必須是相容的,不相互矛盾的。邏輯自洽性也要求構建理論過程當中的全部邏輯推理和數學演算正確無誤。邏輯自洽性是一個理論可以成立的必備條件。)

試想下面這個方法:

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

這個函數調用了 SetWidth 和 SetHeight 方法,而且認爲這些函數都是屬於同一個 Rectangle。這個函數對 Rectangle 是能夠工做的,可是若是傳遞一個 Square 參數進去則會發生斷言錯誤。

因此這纔是真正的問題所在:寫這個函數的程序員是否徹底能夠假設更改一個 Rectangle 的 Width 將不會改變 Height 的值?

很顯然,寫這個函數 g 的程序員作了一個很是合理的假設。而傳遞一個 Square 到這樣的函數中才會引起問題。所以,那些已存在的接收 Rectangle 對象指針或引用的函數也一樣是不能對 Square 對象正常操做的。這些函數揭示了對 LSP 原則的違背。此外,Square 從 Rectangle 衍生也破壞了這些函數,因此也違背了 OCP 原則。

有效性不是內在的

這引出了一個很是重要的結論。從孤立的角度看,一個模型沒法本身進行有意義地驗證。模型的正確性僅能經過它的使用者來表達。例如,孤立地看 Square 和 Rectangle,咱們發現它們是自洽的而且是有效的。但當咱們從一個對基類作出合理假設的程序員的角度來看待它們時,這個模型就被打破了。

所以,當考慮一個特定的設計是否合理時,決不能簡單的從孤立的角度來看待它,而必須從該設計的使用者的合理假設的角度來分析

到底哪錯了?

那麼到底發生了什麼呢?爲何看起來很合理的 Square 和 Rectangle模型變壞了呢?難道說一個 Square 是一個 Rectangle 不對嗎?is-a 的關係不存在嗎?

不!一個正方形能夠是一個長方形,但一個 Square 對象絕對不是一個 Rectangle 對象。爲何呢?由於一個 Square 對象的行爲與一個 Rectangle 對象的行爲是不一致的。從行爲的角度來看,一個 Square 不是一個 Rectangle !而軟件設計真正關注的就是行爲(behavior)。

LSP 原則使咱們瞭解了 OOD 中 is-a 關係是與行爲有關的。不是內在的私有的行爲,而是外在的公共的行爲,是使用者依賴的行爲。例如,上述函數 g 的做者依賴了一個基本事實,那就是 Rectangle 的 Width 和 Height 彼此之間的變化是無依賴關係的。而這種無依賴的關係就是一種外在的公共的行爲,而且其餘程序員有可能也會這麼想。

爲了仍然遵照 LSP 原則,並同時符合 OCP 原則,全部的衍生類必須符合使用者所期待的基類的行爲

契約式設計(Design by Contract)

Bertrand Meyer 在 1988 年闡述了 LSP 原則與契約式設計之間的關係。使用契約式設計,類中的方法須要聲明前置條件和後置條件。前置條件爲真,則方法才能被執行。而在方法調用完成以前,方法自己將確保後置條件也成立。

咱們能夠看到 Rectangle 的 SetWidth 方法的後置條件是:

1 Contract.Ensures((_width == w) && (_height == Contract.OldValue<double>(_height)));

爲衍生類設置前置條件和後置條件的規則是,Meyer 描述的是:

…when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.

換句話說,當經過基類接口使用對象時,客戶類僅知道基類的前置條件和後置條件。所以,衍生類對象不能期待客戶類服從強於基類中的前置條件。也就是說,它們必須接受任何基類能夠接受的條件。並且,衍生類必須符合基類中所定義的後置條件。也就是說,它們的行爲和輸出不能違背任何已經與基類創建的限制。基類的客戶類毫不能對衍生類的輸出產生任何疑惑。

顯然,後置條件 Square::SetWidth(double w) 要弱於 Rectangle::SetWidth(double w),由於它不符合基類的中的條件子句 "(_height == Contract.OldValue (_height))"。因此,Square::SetWidth(double w) 違背了基類定立的契約。

有些編程語言,對前置條件和後置條件有直接的支持。你能夠直接定義這些條件,而後在運行時驗證系統。若是編程語言不能直接支持條件定義,咱們也能夠考慮手工定義這些條件。

總結

開放封閉原則(Open Closed Principle)是許多面向對象設計啓示思想的核心。符合該原則的應用程序在可維護性、可重用性和魯棒性等方面會表現的更好。里氏替換原則 則是實現 OCP 原則的重要方式。只有當衍生類可以徹底替代它們的基類時,使用基類的函數纔可以被安全的重用,而後衍生類也能夠被放心的修改了。

參考資料

相關文章
相關標籤/搜索