子型別必須能替換其基底型別:用基底類別參考的函式,在不知情下換成衍生類別的物件,行為仍要正確。由 Barbara Liskov 提出。它重新定義了 OO 裡「IS-A」的真義——IS-A 是關於行為的,不是關於數學分類的。違反 LSP 是一種潛伏的 OCP 違反。
🧠 When to Use
TIP
訊號:客戶端出現 if (x is SubType) 這種型別檢查;衍生類別覆寫了方法卻實作成空;或子類拋出父類不會拋的例外——這些都在說「這個繼承其實不成立」。
- 正在用繼承表達 IS-A,想確認子型別在客戶端眼中真的可替換。
- 客戶端被迫針對某個子類做特判——通常根源是 LSP 被違反。
- 想用契約(前置 / 後置條件)或共用單元測試來驗證繼承是否合法。
⚖️ Structure & Variants
經典陷阱 — 正方形 IS-A 矩形?
// 數學上 Square IS-A Rectangle,於是直覺地繼承
open class Rectangle(open var width: Double, open var height: Double) {
fun area() = width * height
}
class Square(side: Double) : Rectangle(side, side) {
override var width: Double
get() = super.width
set(v) { super.width = v; super.height = v } // 為維持不變式,連動改高
override var height: Double
get() = super.height
set(v) { super.width = v; super.height = v }
}
// 對 Rectangle 完全合理的客戶端假設:設寬不會動到高
fun g(r: Rectangle) {
r.width = 5.0; r.height = 4.0
check(r.area() == 20.0) // Rectangle 通過;傳 Square 進來 → 面積 16,斷言失敗
}Square自己看沒錯,錯在它作為Rectangle的替代品破壞了g對基底型別的合理假設。有效性不是內在的,要從客戶端角度判斷。
Design by Contract — 前置放寬、後置收緊
- Bertrand Meyer 的契約式設計給了精確判準:衍生方法的前置條件只能相同或更弱,後置條件只能相同或更強。
Rectangle.width的 setter 有隱含後置條件「高不變」;Square的 setter 改了高,違反後置契約,所以違反 LSP。- 實務落地:把基底類別的契約寫成單元測試,這組測試同時套用到所有衍生類別——子類過不了父類的測試,就是 LSP 被破。
修法 — 分解而非繼承(Factoring)
// Line / LineSegment 也一樣:別互相繼承,抽出共同基底
abstract class LinearObject { /* 共同行為 */ }
class Line : LinearObject() { /* 無限延伸 */ }
class LineSegment : LinearObject() { /* 有端點 */ }- 找出真正共用的行為上提到基底,各自加獨特行為——這叫分解(factoring),比硬湊 IS-A 健康。容器類別(
PersistentSet想限制元素型別)同理:用組合或平行實作,別繼承後在add裡拋例外。
⚠️ Misuse & Anti-patterns
- 退化函式:子類覆寫父類方法卻留空實作——等於宣告「我不需要這個行為」,暗示 IS-A 根本不成立。
- 從子類拋父類不拋的例外:逼客戶端知道子類存在並特殊處理,直接打破替換性、連帶破 OCP。
- 只看數學分類就繼承:Square 數學上是 Rectangle,行為上不是。OO 的 IS-A 定義是「對 B 合理的操作,對 A 也都合理」。
🔑 Takeaways
- IS-A 是關於行為的,而行為由客戶端的合理期望定義。
- 違反 LSP → 客戶端被迫認得子類細節 → 破壞 OCP。
- 用 Design by Contract(前置放寬、後置收緊)驗證;擺不平就用組合取代繼承或分解出共同基底。
No notes yet — jot your takeaways or Q&A here.