Liskov Substitution Principle

在軟體設計中,繼承 (Inheritance) 的發生是再正常不過的事了,作為結構化的系統,我們可以視其為多個組件裝載而成的,當結構益龐大起來,就會想將行為類似的程式碼重用,而有了繼承的概念,但這也造成父類與子類使用上耦合造成的問題,繼承不應該只是為了「重複使用程式碼」而存在,而是應該基於行為上的一致性
因此里氏替換原則 (LSP, Liskov Substitution Principle) 就是在提醒我們
👉 子類別必須能夠替代父類別而不影響系統的正確性
換句話說,當一個地方期待父類別時,若換成子類別,程式應該還是能正常運作、不會出現違反預期的行為


子類別不能比父類別更挑剔輸入。想像你去餐廳,主廚說:「你隨便帶食材來,我都能幫你做菜。」結果徒弟廚師說:「不行!你只能帶牛肉來,不然我不煮。」這就是把條件變得更嚴格,使用者本來以為能帶什麼都行,結果換成子類卻被限制了

子類別的輸出不能比父類別更弱,至少要保證一樣的結果。父類(物流公司)承諾:「只要你下單,我一定送達貨物,而且保證完好無損。」子類(加盟快遞)卻說:「嗯… 我只能保證幫你送,東西壞掉不關我的事。」這樣就削弱了原本的承諾,使用者會覺得契約被打折

父類別所承諾的規則,子類別不能破壞。父類(紅綠燈)規則是「綠燈才能走,紅燈一定停」。子類(某路口的燈號系統)卻偷偷改成「紅燈也可以偶爾走一下」。這樣會讓所有使用者混亂,因為他們以為規則始終一致,但事實上卻被打破了

假設我們有一個 SumCalculator,專門計算整數陣列的總和
1 | public class SumCalculator |
現在我想做一個「只計算偶數總和」的版本:
1 | public class EvenNumbersSumCalculator : SumCalculator |
問題來了
1 | SumCalculator evenSum = new EvenNumbersSumCalculator(numbers); //// 呼叫父類別版本,結果卻是 40 |
當我把子類別當成父類別使用時,得到的卻是錯誤的行為。這違反了 LSP,因為 子類別無法取代父類別而保持一致性

為了避免這種問題,我們應該 使用多型 (Polymorphism),讓父類別行為能被覆寫 (override)
1 | public class SumCalculator |
現在,即使我們用父類別的參考,依然能得到正確的結果

其實更合理的設計是從一開始就抽象化,把「計算」這件事定義成契約
1 | public abstract class Calculator |
然後不同的子類別各自去實作自己的邏輯
1 | public class SumCalculator : Calculator |
LSP 關注的是「行為」而不是「程式碼重用」子類別繼承父類別時,最重要的是「能否保持契約一致」
假設我們有一個 付款系統
1 | public interface IPaymentProcessor |
我們希望未來可以新增不同付款方式(信用卡、Line Pay、Apple Pay),而不用修改核心邏輯 → OCP
注入依賴(DI)
1 | public class CheckoutService |
CheckoutService 不關心是哪一種付款,只透過介面操作 → 依賴注入(DI)。
建立實作(LSP 是否被遵守)
1 | public class CreditCardPayment : IPaymentProcessor |
這些類別都可以被 CheckoutService 注入,彼此可替代 → LSP 成立,系統保持穩定

當 LSP 被破壞
1 | public class FraudulentPayment : IPaymentProcessor |
現在如果有人透過 DI 注入 FraudulentPayment
1 | var checkout = new CheckoutService(new FraudulentPayment()); |
FraudulentPayment 雖然「語法上」符合介面,但「行為上」卻無法替代其他實作。這破壞了 LSP。一旦 LSP 被破壞,DI + OCP 的組合就失效,因為我們沒辦法保證「注入什麼實作都能正常工作」





