Agile Principles Patterns Practices in C# (10)

SOLID - 里氏替換原則

Posted by Jasper & Ken on Saturday, December 10, 2022

TOC

LiSkov Principle

子型態(Subtype)必須能夠替換掉它們的基底型態(base type) 當一個函式f(),他的參數為指向某個基底類別為B的參考,而假設B有衍生類別D,如果把D的物件作為B型態傳遞給f(),若會導致f()錯誤,則代表此設計違反了LSP。

1. 違反LSP的情境

  • 違反了OCP,因為DrawShape()這個動作必須要知道Shape的所有類別,才可以透過if/else…的方式判斷,且每次建立新類別時,都必須要進行重大修正
  • 違反了LSP,因為Square / Circle類別無法替換Shape類別
  • 通常對於LSP的違反也潛在的違反了OCP
struct Point {double x,y}

public enum ShapeType {square,circle};

public class Shape
{
    private ShapeType type;

    public Shape (ShapeType t){type= t; }
    public static void DrawShape(Shape s)
    {
        if(s.type == ShapeType.square)
            (s as Square).Draw();
        else if(s.type == ShapeType.circle)
            (s as Circle).Draw();    
    }
    public class Circle : Shape
    {
        private Point center;
        private double redius;

        public Circle() : base(ShapeType.circle) {}
        public void Draw(){}
    }

    public class Square : Shape
    {
        private Point topLeft;
        private double side;

        public Square() : base(ShapeType.square) {}
        public void Draw(){}
    }
}
  • 使用Rectangle物件的函式,無法正確的操作Square物件。對於這些函式來說,Square不能替換Rectangle,故可知這樣是違反LSP的。
public class Rectangle
{
    private Point topLeft;
    private double width;
    private double height;
    
    public virtual double Width
    {
        get { return width; }
        set{ width = value; }
    }

    public virtual double Height
    {
        get { return height; }
        set{ height = value; }
    }
}

public class Square : Rectange
{
    public override double Width
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }

    public override double Height
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }
}
  • 有效性非本質屬性:

    • 一個模型,如果獨立來看待,並不具有真正意義上的有效性。
    • 模型的有效性只能透過他的客戶程式來表現。
    • 在判斷某個特定設計是否恰當時,不能完全獨立看待這個解決方案。必須根據該設計的使用者所做出的合理假設來審查。
    • 不需要預測所有的假設,只需要預測那些最明顯違反LSP的情況。
  • IS-A是關於行為的:

    • 從行為方式的角度來看,Square不是Rectangle,物件的行為才是軟體真正關注的問題。
    • OOD中IS-A的關係是從行為來判斷的,而行為是可以合理假設的,也是客戶程式所依賴的
  • 基於契約設計:

    • 基於契約的設計(Design by contract):使合理的假設明確化。必須要滿足前置/後置條件
      • 宣告前置條件 (precondition): 要使一個方法可以執行,前置條件必為真。
      • 宣告後置條件 (postcondition):方法執行完畢後,該方法要保證後置條件為真。
    • 在重新宣告衍生類別的副程式時,只能使用相等或更鬆的前置條件來替換原始的前置條件。只能使用相等或更強的後置條件來替換原始的後置條件。
// Rectangle.Width postcondition
assert((width == w) && (height == old.value));
  • 在單元測試中指定契約:
    • 可以透過撰寫單元測試的方式來指定契約。
    • 單元測試透過徹底的側是一個類別的行為,使該類別的行為更加清晰。

2. 用提取共同部分的方法代替繼承

  • 只有在極少數的情況下,接受一個多型行為中的微妙錯誤 才會比 試著修改設計使之完全符合 LSP 更有利。
  • 修改前:
// Line.cs
public class Line
{
    private Point p1;

    private Point p2;

    public Line(Point p1, Point p2) {this.p1 = p1; this.p2 = p2; }
    
    public Point P1 {get {return p1;}}
    public Point P2 {get {return p2;}}
    public DOUBLE sLOPE {get {}}
    public double Yintercept {get{}}
    public virtual bool IsOn(Point p) {} 
}


// LineSegment.cs

public class LineSegment : Line
{
    public LineSegment(Point p1, Point p2) :base(p1,p2)  {}

    public double Length() {get{}}
    public override bool IsOn(Point p) {}
}
  • 修改後:
    • 將類別Line和類別LineSegment的共同部分抽取出來做為一個抽象基底類別
    • 如果每一組類別都支援一個共同的職責,那麼它們應該從一個共同的超類別繼承該職責
// LinearObject.cs
public abstract class LinearObject
{
    private Point p1;
    private Point p2;
    
    public LinearObject(Point p1,Point p2)
    {
        this.p1 = p1;
        this.p2 = p2;
    }

    public Point P1 {get { return p1;}}
    public Point P2 {get { return p2;}}

    public double Slop3 { get {}}
    public double Yintercept {get {}}
    public virtual bool IsOn(Poiont p){}
}

// Line.cs
public class Line :LinearObject
{
    public Line(Point p1,Point p2) : base(p1,p2) {}
    public override bool IsOn(Point p) {}
}

// LineSegment.cs
public class LineSegment :LinearObject
{
    public LineSegment(Point p1,Point p2) : base(p1,p2) {}

    public double GetLength() {}
    
    public override bool IsOn(Point p) {}
}

3. 啟發式規則和習慣用法

  • 以某種方式從基底類別中去除功能的衍生類別
  • 完成的功能少於基底類別的衍生類別,通常是不能替換基底類別的,因為這樣就違反了LSP
  • 在衍生類別中存在退化函式並不總是代表違反了LSP,但是當出現這種情況時,還是值得關注的。
// 衍生類別中的退化函式
public class base
{
    public virtual void f() 
    {
        // Some thing need to do 
    }
}

public class Derived : base
{
    public override void f() {}
}

4. 總結

  • OCP是OOP中許多主張的核心,如果能有效地應用這個原則,應用程式就會具有更強的可維護性、在使用性及強固性。
  • 透過 LSP實現子型態的可替換性,才使得使用基底類別型態表示的模組,在無需修改的情況下就得以擴展。
  • IS-A的涵義比較廣泛,較無法作為子型態的定義。
  • 子型態的正確定義應該是可替換的,這裡的可替換性可以透過顯示或隱式的契約來定義。