Agile Principles, Patterns, and Practices in C# 無瑕的程式碼 敏捷完整篇 第10章 LSP:Liskov替換原則
第十章內容是LSP:Liskov替換原則,要遵守LSP才能有OCP的效果。
OCP背後主要機制是抽象和多型,C#支援抽象和多型的關鍵是繼承。如何建立適當的繼承關係,讓程式能夠符合OCP,答案就是LSP:Liskov替換原則。
Liskov 替換原則 (LSP)
子型態(subtype)必須能夠替換掉它們的基底型態 (base type)
假設有個函數f使用基底類別B,B有個衍生類別D,若D傳給f會產生錯誤行為,代表程式違反LSP。
若f的編寫者在D加入判斷條件修正錯誤,讓f能夠有正確的行為,這種作法違反OCP。 f對B的所有不同衍生類別都不再是封閉。
違反LSP的情形
一個簡單的範例
Listing10-1 基底類別Shape沒有寫好,每新增一個衍生類別就必須修改基底類別。
原因在於工程師不了解多型,誤認為多型會拖慢程式。
更微妙的違反情形
正方形繼承矩形的案例也會違反LSP。
繼承是IS-A的關係,正方形可繼承矩形。
正方形並不同時需要成員變數高度與寬度,但正方形仍然會從矩形繼承高度與寬度。原本接受矩形物件的函式,設定高度與寬度會讓正方形的高度與寬度改變,違反LSP。
可將setter屬性宣告為virtual可解問題,然而這樣做會影響基底類別,違反OCP。
Listing10-3是修改後的結果,基類別Rectangle寬度與高度設定為virtual ,衍生類別Square類別寬度與高度設定為override修改基底類別的寬度與高度。
真正的問題
接受矩形參數的函數g,輸入正方形還是會出現例外。
有效性並非本質屬性
這段大意是有沒有符合LSP要看宏觀整體,而不是微觀單一類別。Listing10-3 滿足矩形與方形的相容性,遇到函數g的設計者「對基底類別做出一些合理的假設」就會「破功」。
與第九章OCP情況相同,不要對未來過度猜想,出現狀況再改。
編者補充TDD測試能預測到未來一些變化。
IS-A是關於行為的
LSP指出繼承IS-A的關係是從行為來判斷,而行為可以進行合理假設。從行為的角度來看正方形不是矩形。
基於契約的設計 (Design by contract)
如何知道什麼是「合理的假設行為」?解決方法基於契約的設計DBC。
契約替每個方法宣告前置條件和後置條件。一個方法得以執行,前置條件必須為真,執行完後該方法要保證後置條件為真。
衍生類別必須和基底類別的所有後置條件一致。
在單元測試中指定契約
可透過撰寫單元測試來指定契約,可知道對於要使用的類別,應該做出什麼合理的假設。
一個真實世界的例子
動機
看圖10-2、Listing10-4與Listing10-5大約可知作者想要表達的內容。客戶可使用PrintSet函數不用關心是Unbounded Set 或是 Bounded Set 。
問題
參考圖10-3 Third-Party Persistent Set 只接受特殊的PersistentObject,Listing10-6 Add函數有可能會出現例外,客戶程式沒辦法知道它所加入的元素是否應該衍生自PersistentObject?
不符合LSP的解決方案
????????
只知作者用「契約」的方式解決,將PersistentObject與PersistentSet隱藏起來。
符合LSP的解決方案
參考圖10-4,承認PersistentSet與Set不存在 IS-A關係,PersistentSet與Set改當「兄弟」。
用提取共同部分的方法代替繼承
標題已經說明這段內容,Listing10-7 Line類別與Listing10-8 LineSegment類別的關係違反LSP。
解決方法將兩個類別共同部分提取出來成為一個超類LinearObject,讓Line類別與LineSegment類別繼承LinearObject超類。
啟發式規則和習慣用法
衍生類別通常功能都比基類別功能多,參考Listing10-13出現退化函數,有可能出現違反LSP的情況。
編者補充java版還有提到一種違反LSP的情況,衍生類別拋出基底類別不存在的例外。
總結
LSP是使OCP成為可能的主要原則之一。
術語IS-A含意過於廣泛,無法作為子型態的定義。子型態正確的意義應該是可替換的,可透過契約定義替換性。
留言
張貼留言