1. 里氏替換原則 Liskov Substitution Principle
2. 關於LSP的基本精神
當實作繼承時,必須確保型別轉換後還能得到正確的結果
每個衍生類別都可以正確地替換為基底類別,且程式在執行時不會有異常的情況(如發生執行時期例外)
必須正確的實作”繼承
“與”多型
“
3. 常見的設計問題 3.1. 不正確的實作”繼承”與”多型”
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(); } } 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 ; } public override void Add () { throw new Exception("Not allowed" ); } }
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在編譯的時期就可以明顯的看出錯誤。
3.2.2. 範例程式:企鵝不會飛 問題描述
違反 LSP 的原因在於Penguin 無法正常替換 Bird ,因為 Penguin 的 Fly
方法引發異常,這不符合使用父類別的預期行為。
修改後的解決方案
可以透過引入抽象類別或接口來重新設計,將具有飛行能力的鳥與其他鳥類分開。
修改前
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 () { throw new NotSupportedException("Penguins cannot fly!" ); } } public class Program { public static void Main (string [] args ) { Bird bird = new Penguin(); bird.Fly(); } }
修改後
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(); } } } }
修改後的設計優點
分離行為 :使用 IFlyable
接口表示飛行能力,不將 Fly
方法強加於所有鳥類。
符合 LSP :每個子類別完全符合父類別的行為預期。
開閉原則 :新增其他類型的鳥類時不需要修改現有類別,僅需實現適當的接口即可。
這樣的設計更靈活且符合 SOLID 原則。
4. 關於LSP的實作方式
採用類別繼承方式
來進行開發
採用合約設計方式
來進行開發
利用介面
(interface)來定義基底型別(base type)
大部分的人還滿喜歡用介 面,因為介面最大的好處就是耦合度很低,因為它只能定義方法,不能定義任何東西。
5. 關於LSP的使用時機
6. LSP討論事項
在教導新人時,如何有效的避免繼承的錯誤實作?
你會用抽象類別
、類別
或介面
來實現LSP原則?為什麼?
教大家一個小技巧,把警告看完,強迫讓warning必須為0