1. 程式人生 > 其它 >SOLID學習筆記 - 里氏替換原則

SOLID學習筆記 - 里氏替換原則

六大設計原則

  里氏替換原則:

    “子型別必須可替代其基本型別”。換句話說,給定一個特定的基類,從它繼承的任何類都可以替代基類。

    錯誤示例:

class LSPDemo
{
    static void Main(string[] args)
    {
        Rectangle shape;
        shape = new Rectangle();
        shape.SetWidth(14);
        shape.SetHeight(
10); Console.WriteLine("Area={0}", shape.Area); // 140 shape = new Square(); shape.SetWidth(14); shape.SetHeight(10); Console.WriteLine("Area={0}", shape.Area); // 100 Console.ReadLine(); } } public class Rectangle { protected int _width; protected
int _height; public int Width { get { return _width; } } public int Height { get { return _height; } } public virtual void SetWidth(int width) { _width = width; } public virtual void SetHeight(int height) { _height = height; }
public int Area { get { return _height * _width; } } } public class Square : Rectangle { public override void SetWidth(int width) { _width = width; _height = width; } public override void SetHeight(int height) { _width = height; _height = height; } }

執行此程式碼將導致第一個形狀的面積為 140,第二個形狀的面積為 100。然後,您會想啟動偵錯程式以找出發生了什麼。子類的行為已更改。用 Uncle Bob 的話來說,程式碼違反了 LSP,因為子型別不能替代其基型別。現在我們都知道正方形和矩形是不一樣的,但這表明正方形不是矩形的特殊型別。

這是大多數關於LSP的討論停止的地方。但是 LSP 比這個簡短的示例更復雜。LSP 實際上執行了幾條規則。這些規則分為兩類,合同規則和差異規則。讓我們更深入地研究一下這些規則。

合同規則

建立類時,協定以正式術語說明如何使用該物件。您需要將方法的名稱以及這些引數的引數和資料型別作為輸入,然後期望將哪種資料型別作為返回值。LSP 對合同規則施加了三個限制:

· 子型別無法加強前置條件 – 前置條件是方法可靠執行所需的條件。這將要求正確例項化類,並將所需的引數傳遞給方法。通常,保護子句用於強制引數具有正確的值。

· 後置條件不能在子型別中被削弱 – 後置條件驗證當方法返回時物件是否處於可靠狀態。保護子句再次用於強制執行後置條件。

· 超型別的不變數必須由子型別保留 – 不變數是物件構造完成後,在物件的生存期內必須保持為真的事物。這可能是在建構函式中設定並假定不會更改的欄位值。子型別不應將這些型別的欄位更改為無效值。例如,可能存在一個業務規則,即最低運費欄位必須大於或等於零。子型別不應使其成為負數。只讀欄位保證遵循此規則。

超差規則

在解釋方差規則之前,我們需要定義方差。以下是維基百科所說的,“方差是指更復雜型別之間的子型別如何與其元件之間的子型別相關聯......C# 介面中的方差由其型別引數上的輸入/輸出註釋確定“https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)”。這相當複雜。可以這樣想,你期望不同但相關的亞型如何表現?這就是差異。現在,以下是 LSP 差異規則:

·子型別中必須存在方法引數的逆變 – 子型別反轉型別的順序。

·子型別中方法的返回型別必須存在協方差 – 子型別使型別保持從特定到最泛型的相同順序。

·子型別不應引發新的異常,除非這些異常是基型別引發的異常的子型別 – 此規則簡單且不言自明。

如果推斷這些規則,則會發現子型別無法接受更具體的型別作為引數,並且不能返回不太具體的型別作為結果。

 

利斯克的解決方案

現在我已經展示了LSP不是什麼並解釋了它是什麼,我想向您展示一個氏原理的例子。問題是,我們需要用正方形代替矩形,反之亦然,只改變例項化的類。但是,在建立正方形時,如果我們傳遞兩個高度和寬度引數,那麼哪個引數是正確的呢?我們可以要求您傳遞相同的數字兩次,但這似乎毫無用處,因為只需要一個。歸根結底,上面的程式碼可以很好地顯示Liskov不是什麼,但是當您嘗試顯示Liskov是什麼時,它不能很好地工作。所以,我必須轉向另一個例子。

我決定使用基本型別的動物。當然,這不是一個完美的例子,因為有些動物跳躍,有些動物滑行,行走或疾馳。有些會飛,有些則不會。有些人有腳,有些人沒有。但我認為它仍然會顯示如何將一個子類替換為另一個子類。

class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Dog();
        Console.WriteLine(animal.Walk());
        Console.WriteLine(animal.Run());
        Console.WriteLine(animal.Fly());
        Console.WriteLine(animal.MakeNoise());
        Console.ReadLine();
    }
}

public class Animal
{
    public string Walk()
    {
        return "Move feet";
    }
            
    public string Run()
    {
        return "Move feet quickly";
    }

    public virtual string Fly()
    {
        return null;
    }

    public virtual string MakeNoise()
    {
        return null;
    }
}

public class Dog : Animal
{
    public override string MakeNoise()
    {
        return "Bark";
    }
}

public class Bird: Animal
{
    public override string MakeNoise()
    {
        return "Chirp";
    }

    public override string Fly()
    {
        return "Flag wings";
    }
}

如果為 Dog 執行此程式碼,則將獲得以下輸出。

Move feet
Move feet quickly

Bark

請注意 Fly( ) 的空行。狗不會飛,所以什麼都沒回。現在把狗改成鳥,你得到

Move feet
Move feet quickly
Flag wings
Bark

如果您現在將Bird更改為Animal,會發生什麼情況?

Move feet
Move feet quickly

子類(狗或鳥)可以替換為基類(動物),一切仍然有效。程式碼本身並不關心。我們還可以更進一步,使用 Factory 方法來建立我們想要使用的類,而程式碼甚至根本不知道該類。但這超出了這個討論的範圍。