Liskov 替換原則 (LSP)

★★★★Studied

FromAgile Principles, Patterns, and PracticesFunctional Design

📚 From the Books

Liskov 替換原則 (LSP)ConceptPrinciples

子型別必須能替換其基底型別:用基底類別參考的函式,在不知情下換成衍生類別的物件,行為仍要正確。由 Barbara Liskov 提出。它重新定義了 OO 裡「IS-A」的真義——IS-A 是關於行為的,不是關於數學分類的。違反 LSP 是一種潛伏的 OCP 違反。

🧠 When to Use

TIP

訊號:客戶端出現 if (x is SubType) 這種型別檢查;衍生類別覆寫了方法卻實作成空;或子類拋出父類不會拋的例外——這些都在說「這個繼承其實不成立」。

⚖️ 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

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading