0%

SOLID 筆記 - (6)里氏替換原則

1. 里氏替換原則 Liskov Substitution Principle

  • Subtypes must be substitutable for their base types.

    • subtypes(衍生型別) = 類別
    • base types(基底型別) = 介面、抽象類別、基底類別
  • 子型別必須可以替換為他的基底型別

  • 如果你的程式有採用繼承或介面,然後建立出幾個不同的衍生型別(Subtypes)。在你的系統中只要是基底型別出現的地方,都可以用子型別來取代,而不會破壞程式原有的行為。

2. 關於LSP的基本精神

  • 當實作繼承時,必須確保型別轉換後還能得到正確的結果
    • 每個衍生類別都可以正確地替換為基底類別,且程式在執行時不會有異常的情況(如發生執行時期例外)
    • 必須正確的實作”繼承“與”多型

3. 常見的設計問題

3.1. 不正確的實作”繼承”與”多型”

  • 保哥語錄

    • 程式是不斷的維護,越改越爛(笑死),程式會隨著時間的演進,程式品質下降,學這些原則就是要在code的過程,維持我們的品質

    • 衍生類別轉基底型別的時候,不應該出事情

3.1.1. 範例程式:計算矩形面積

第一版:沒有繼承,單純計算矩形面積

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Program
{
static void Main()
{
Rectangle o = new Rectangle();
o.Width = 40;
o.Height = 50;
LSPBehavior.GetArea(o).Dump();
}
// Define other methods and classes here
}

public class Rectangle
{
public int Height { get; set; }
public int Width { get; set; }
}

public class LSPBehavior
{
public static int GetArea(Rectangle s)
{
if (s.Width > 20) s.Width = 20;
return s.Width * s.Height;
}
}

請告訴我這個值,執行下去會是多少?

1000

第二版:新增需求,增加Square類別(套用OCP原則)

增加需求Square

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Program
{
static void Main()
{
Square o = new Square();
o.Width = 40;
o.Height = 40;
LSPBehavior.GetArea(o).Dump();
}
}

public class Rectangle
{
public int Height { get; set; }
public int Width { get; set; }
}

public class Square : Rectangle
{
private int height;
private int width;

public int Height
{
get { return height; }
set { height = width = value; }
}

public int Width
{
get { return width; }
set { width = height = value; }
}
}

public class LSPBehavior
{
public static int GetArea(Rectangle s)
{
if (s.Width > 20) s.Width = 20;
return s.Width * s.Height;
}
}

請問Square套用了什麼原則?(OCP)

請告訴我這個值,執行下去會是多少?

0

第三版:重構程式,正確套用LSP原則

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Program
{
static void Main()
{
Square o = new Square();
o.Width = 40;
o.Height = 40;
LSPBehavior.GetArea(o).Dump();
}
}

public class Rectangle
{
public virtual int Height { get; set; }
public virtual int Width { get; set; }
}

public class Square : Rectangle
{
private int height;
private int width;

public override int Height
{
get { return height; }
set { height = width = value; }
}

public override int Width
{
get { return width; }
set { width = height = value; }
}
}

public class LSPBehavior
{
public static int GetArea(Rectangle s)
{
if (s.Width > 20) s.Width = 20;
return s.Width * s.Height;
}
}

3.2. 實作繼承時,在特定情況下發生執行時期錯誤(Runtime Error)

違反LSP原則有時候較難發現

  • 保哥語錄

    • 因為老闆說出現例外,會罵你,於是…try catch ,然後catch 是空的,被迫寫出糟糕的code
    • 最好的狀況,我們寫一段code,在編譯的時候就能夠看出錯誤,這才是好的code, 而不是等run到那一項才掛掉。

3.2.1. 範例程式:會員折扣

修改前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class Program
{
static void Main()
{
List<Customer> Customers = new List<Customer>();
Customers.Add(new SilverCustomer());
Customers.Add(new GoldCustomer());
Customers.Add(new Enquiry());

foreach (Customer o in Customers)
{
o.Add();
}
}
}

class Customer
{
public virtual double getDiscount(double TotalSales)
{
return TotalSales;
}

public virtual void Add()
{
}
}

class GoldCustomer : Customer
{
public override double getDiscount(double TotalSales)
{
return base.getDiscount(TotalSales) - 5;
}

public override void Add()
{
Console.WriteLine("GoldCustomer: Add");
}

}


class SilverCustomer : Customer
{
public override double getDiscount(double TotalSales)
{
return base.getDiscount(TotalSales) - 5;
}

public override void Add()
{
Console.WriteLine("SilverCustomer: Add");
}
}


class Enquiry : Customer
{
public override double getDiscount(double TotalSales)
{
return base.getDiscount(TotalSales) - 5;
}

//Enquiry它不是客戶,只是想用getDiscount方法。
//所以Add方法不使用,故意丟一個例外的寫法
public override void Add()
{
throw new Exception("Not allowed");
}
}

img

Enquiry它不是客戶,只是想用getDiscount方法。所以Add方法不使用,故意丟一個例外的寫法

修改後

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class Program
{
static void Main()
{
List<Customer> Customers = new List<Customer>();
Customers.Add(new SilverCustomer());
Customers.Add(new GoldCustomer());
Customers.Add(new Enquiry());

foreach (Customer o in Customers)
{
o.Add();
}
}
}

interface IDiscount
{
double getDiscount(double TotalSales);
}

interface IDatabase
{
void Add();
}

class Customer : IDiscount, IDatabase
{
public virtual double getDiscount(double TotalSales)
{
return TotalSales;
}

public virtual void Add()
{
}
}


class GoldCustomer : Customer
{
public override double getDiscount(double TotalSales)
{
return base.getDiscount(TotalSales) - 5;
}

public override void Add()
{
Console.WriteLine("GoldCustomer: Add");
}

}


class SilverCustomer : Customer
{
public override double getDiscount(double TotalSales)
{
return base.getDiscount(TotalSales) - 5;
}

public override void Add()
{
Console.WriteLine("SilverCustomer: Add");
}
}

class Enquiry : IDiscount
{
public double getDiscount(double TotalSales)
{
return TotalSales - 5;
}
}

Enquiry只實作需要的方法,這樣就有很大的好處,我們的code在編譯的時期就可以明顯的看出錯誤。

img

3.2.2. 範例程式:企鵝不會飛

問題描述

違反 LSP 的原因在於Penguin 無法正常替換 Bird,因為 PenguinFly 方法引發異常,這不符合使用父類別的預期行為。

修改後的解決方案

可以透過引入抽象類別或接口來重新設計,將具有飛行能力的鳥與其他鳥類分開。

修改前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("I can fly!");
}
}

public class Penguin : Bird
{
public override void Fly()
{
// 企鵝不會飛,違反 LSP
throw new NotSupportedException("Penguins cannot fly!");
}
}

public class Program
{
public static void Main(string[] args)
{
Bird bird = new Penguin();
bird.Fly(); // 違反 LSP,因為子類別的行為與父類別不一致
}
}

修改後

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public abstract class Bird
{
public abstract void Display();
}

public interface IFlyable
{
void Fly();
}

public class Sparrow : Bird, IFlyable
{
public override void Display()
{
Console.WriteLine("I am a sparrow.");
}

public void Fly()
{
Console.WriteLine("I can fly!");
}
}

public class Penguin : Bird
{
public override void Display()
{
Console.WriteLine("I am a penguin.");
}
}

public class Program
{
public static void Main(string[] args)
{
List<Bird> birds = new List<Bird>
{
new Sparrow(),
new Penguin()
};

foreach (var bird in birds)
{
bird.Display();

if (bird is IFlyable flyable)
{
flyable.Fly();
}
}
}
}

修改後的設計優點

  1. 分離行為:使用 IFlyable 接口表示飛行能力,不將 Fly 方法強加於所有鳥類。
  2. 符合 LSP:每個子類別完全符合父類別的行為預期。
  3. 開閉原則:新增其他類型的鳥類時不需要修改現有類別,僅需實現適當的接口即可。

這樣的設計更靈活且符合 SOLID 原則。

4. 關於LSP的實作方式

  • 採用類別繼承方式來進行開發
    • 需注意繼承的實作方式
  • 採用合約設計方式來進行開發
    • 利用介面(interface)來定義基底型別(base type)

大部分的人還滿喜歡用介 面,因為介面最大的好處就是耦合度很低,因為它只能定義方法,不能定義任何東西。

5. 關於LSP的使用時機

  • 當你需要透過基底型別多型物件進行操作時

6. LSP討論事項

  • 在教導新人時,如何有效的避免繼承的錯誤實作?
  • 你會用抽象類別類別介面來實現LSP原則?為什麼?

教大家一個小技巧,把警告看完,強迫讓warning必須為0