深入淺出 Liskov 替換原則 Liskov Substitution Principle
March 22, 2020這篇文章介紹軟體架構裡面 S.O.L.I.D 中的L (Liskov Substitution Principle)
這篇文章中大部分的程式碼 參考自SOLID Principles of Object-Oriented Design and Architecture
特別感謝領英Kafka一哥 lmr3796 遠端線上教學
定義
If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program (correctness, task performed, etc.)
這意思就是說 如果你程式碼中的類別T的物件 都可以被一個類別S的物件給取代 而且程式碼還運作正常 那你才可以說類別S是類別T的subType
subType vs subClass
這個原則很容易被人誤解 最大的原因是大多數人搞不清楚subtype跟subclass的差別(我甚至不知道中文怎麼翻 為了不搞混大家這篇一律用英文)
subType: 就如同LSP定義所說 如果S是T的subType 那在所有T出現過的地方都可以用S取代 目標是讓你的架構更加彈性
subClass: A extend B, 那你就可以說B是A的subClass 目標是code reuse
當然subType也是靠繼承來達成 但只有正確的繼承 才夠格被稱為subType 不正確的繼承 就只是個繼承
There are important differences between subtypes and subclasses in supporting reuse. Subclasses allow one to reuse the code inside classes - both instance variable declarations and method definitions. Thus they are useful in supporting code reuse inside a class. Subtyping on the other hand is useful in supporting reuse externally, giving rise to a form of polymorphism. That is, once a data type is determined to be a subtype of another, any function or procedure that could be applied to elements of the supertype can also be applied to elements of the subtype.
我來翻譯翻譯
Subtype = Subclass(realization) which can be substituted for the type it extends(implements)
聽完我舉兩個例子你會更加的清楚
鳥跟鴕鳥例子
我們有個鳥類別
鴕鳥即使不會飛 但也是鳥的一種
在這個例子 Ostrich就是Bird的SubClass 因為他使用了繼承
但Ostrich不是Bird的SubType 因為不是所有有Bird出現的地方你都可以用Ostrich取代 而且程式碼正確性一樣
比如說我得到了一隻鳥
我要是把這個鳥換成鴕鳥 就會有例外噴出來
正方形長方形例子
我們有正方形類別跟長方形類別 因為正方形只是長方形的長寬相同的特例 所以我們讓正方形繼承長方形
當然別忘了在改長寬的時候兩個要一起改
這個例子中 Square 就是 Rectangle 的 SubClass
那Square 是不是 Rectangle 的 SubType 呢 我們來看看這個函式
這個函數原本丟長方形的物件進去會對的 但要是丟了正方形的物件進去就會錯
所以 Square 不是 Rectangle 的 SubType
這有什麼了不起
那如果我違反了會怎麼樣呢 Square 不是 Rectangle 的 SubType 世界會塌嗎?
世界不會塌 但是這不是個好架構 想像一下你是個使用Rectangle跟Square類別的人 當你拿到一個Rectangle的時候
1.因為你不確定當初設計Rectangle的人 有沒有保證每個繼承Rectangle的類別都是Rectangle的subType
2.你知道如果一個Rectangle的subClass不是Rectangle的subType的話 你預期會應該會正確的行為有可能會不正確(比如testRectangle
函數)
那你只能怎麼辦 你只能
天知地知你知我知 這程式太醜 而且使用者被強迫了解太多細節
講到這裡 你應該知道我要說什麼了 你要做好心理準備 這可能會讓你的世界崩塌
準備好了嗎
我要說了喔
就是…
大多數你使用繼承的地方都不該用繼承
主要原因 是因為
Inheritance是所有依賴關係裡面最強的 而你知道太過依賴總是沒啥好事
- lmr3796
所以LSP這個原則告訴了我們兩件事
1.如何判斷A跟B是不是SubType關係
2.只要不是subType關係 你就不該用繼承
如何判斷
那也太麻煩了吧 我每次A繼承B的時候 我都要去想辦法去找出一個例子 把所有的B換成A來看看跑起來對不對 試過所有情況後我才能有信心的認為A是B的SubType嗎?
這是我第一次讀完LSP的結論 我相信也是大多數人的想法
如果你只是看Clean Architecture或是其他網路上文章的話 沒錯 因為這是LSP的定義 可是這篇文章並不是普通的文章 該不該用繼承 有些準則可以遵循 每項準則都遵守了 你就可以用繼承
且聽我娓娓道來
Liskov
先來了解一下 為什麼 S.O.L.I.D 裡面 只有LSP是用人名呢 因為Liskov很猛
Barbara Liskov 他是美國第一位CS女博士 2008年Turing Award得主 在對女性不友好的年代中 還是脫穎而出 絕對是最偉大的程序員之一
而我們講的LSP 就是她寫的paper Behavioral Subtyping Using Invariants and Constraints
我們來看一下本篇Paper最重要的一段 這段定義了何為Subtype
看得懂嗎? 看不懂沒關係 這篇文章就是為此而生的
這段天書 基本上就是在描述7個規則 只要這7個規則都遵守了 那你繼承用下去 就是subType
7個規則
規則一: Covariance of argument
1.當你實作或是繼承一個superClass的方法時 你方法的input argument的數目應該一樣
2.在SubClass的方法裡面的argument 都應該是SuperClass相對應的argument的SuperType
為了方便講解 我們先隨便定義一個SuperType <- Type <- SubType的關係
上例子 我有一個DemoClass
如果我想要繼承它的話 裡面的方法參數的數目要一樣
而且如果compute()
有參數的話
那你override的時候 參數只能是SuperType 不能是SubType
這就是第一個規則
腦中的小疑問
誒你在開玩笑嗎 如果要Override的話 所有的argument參數要同一種型態啊 不然compiler不會給你過的吧
那是因為我們現在寫的是Java 是個強型別的語言 Liskov這篇paper是可以套用在所有的程式語言 很多語言沒有這種check 那你就必須要自己檢查有沒有違反以上的rule
規則二: Covariance of result
1.當你實作或是繼承一個superClass的方法時 你方法的回傳的數目應該一樣
2.在SubClass的方法裡面的回傳類別 都應該是SuperClass相對應的回傳值的SubType
夠好懂 如果compute()
有回傳值的話
那你override的時候 回傳值只能是SubType 不能是SuperType
這就是第二個規則
規則三: Exception rule
當你實作或是繼承一個superClass的方法時 拋出的例外 必須是superClass方法拋出的例外的subType
這就是第三個規則
深呼吸喘口氣
恭喜你 你看完前三個原則了 現在問題來了 要不要死背!
做學問要融會貫通 其實如果你真的懂subType的好處 這些都是可以推導的出來
subType的好處是什麼呢 好處就是我們可以放心的相信polymorphism
比如說我得到了一隻鳥
今天我把這個物件b
換成任何一個Bird的subType的物件
我的程式還是會對 不用擔心會來個鴕鳥導致我的fly()噴出例外
我們的目標 是讓使用者(Client)對於這個類別(Bird)了解的越少越好 他不需要去仔細看所有鳥的SubType來決定它的程式怎麼寫
只要 他回傳給我的一定是鳥的subType
只要 他可能拋出的exception是原本預期的exception的subType
那我就不用另外處理 降低了依賴 Client輕鬆愉快
希望這個時候你有一點點融會貫通的感覺 記得這個感覺 讓我們繼續看下去
規則四: Pre-condition rule
先了解什麼是pre-condition
Pre-condition: An assertion about the state of the system before the method is called
比如說 你要執行methodA之前 你必須要先initilize某個類別X 那這就是這個methodA的pre-condition
了解什麼是pre-condition之後 規則四講的是
subType的函式需要的precondition 不能比baseType的還要嚴謹(strict)
你也可以想成 在所有可以執行baseType.methodA的地方 你都應該可以執行subType.methodA
這就是第四個規則
規則五: Post-condition rule
先了解什麼是post-condition
Post-condition: An assertion about the state of the system after method execution completes
比如說執行完methodA之後 某個物件一定不能是null 就是一個methodA的post-condition
了解什麼是post-condition之後 規則五講的是
subType的函式需要的postcondition 不能比baseType的還要鬆散(weak)
比如說baseType的methodA的post-condition是: 某個物件一定不能是null 那如果一個繼承baseType的類別的methodA的post-condition是: 某個物件可能是null
那這個類別就不是baseType的subType 因為這個post-condition比baseType的更加的不嚴謹
這其實也很直覺 當你的使用者在用baseType的methodA 他會預期跑完後會有些預期的狀況會發生 但要是因為你隨便繼承 導致subClass的post-condition更不嚴謹 那使用者的程式可能就會報錯 比如他沒預期會有null 結果你給null 那就噴NPE
這就是第五個規則
規則六: Invariant rule
先了解什麼是invariant
Some assertion about a specific class property which is always true
就是對於一個類別的永恆不變的法則 比如說對於一個Queue來說
number of elements in the queue <= capacity
這就可以說是一個invariant
了解什麼是invariant之後 規則六講的是
一個subtype的類別的invariant 必須包含所有baseType的invariant
比如說我們今天想寫一個Queue的subType 假設是BoundedQueue好了 那BoundedQueue必須要有所有Queue的invariant
這就是第六個規則
規則七: Constraint rule
先了解什麼是Constraint 他跟invariant的差別非常的細微
Some assertion about how class property evolves over time
就是隨著時間變化 某些class property應該怎麼改變(或是不改變)
比如說對於一個Queue來說
capacity never changes after initialized
這就是一個queue的constraint
Constraint vs Invariant
請容許我再特別說明一下這兩個的差別 因為我一開始也是搞不清楚
Invariant 指的是無論何時何地 你單看一個類別的instance 都應該要遵守的 就叫做invariant
Constraint 指的是對於某一個物件來說 在歷史上的兩個不同時間 應該要遵循什麼事情
來個例子 假設我有個針對Message找error的類別MessageErrorDetector
MessageErrorDetector類別 只要曾經找到過error 那麼之後每次呼叫isErrorDetected
就只會回傳true
那MessageErrorDetector類別的constraint就是: 只要曾經偵測到錯誤訊息 那這個物件就永遠停留在這個error狀態
希望這個例子有讓你更明白何謂constraint
再看回規則七
了解什麼是constraint之後 規則七講的是
一個subtype的類別的constraint 必須包含所有baseType的constraint
其實就是 你要respect你要繼承的類別的constraint
假設你要繼承MessageErrorDetector 寫了ResetableMessageErrorDetector
感覺很合理啊 我想要一個功能性更強的MessageErrorDetector 繼承一下 多加個功能錯了嗎
ㄟ 還真的錯了 Liskov告訴你說 這樣子違反了LSP的規則七
只要曾經偵測到錯誤訊息 那這個物件就永遠停留在這個error狀態 這個constraint在子類中不成立了
所以ResetableMessageErrorDetector並不是MessageErrorDetector的SubType
出生到現在 第一次讀懂Liskov
如果你讀到了這裡 那我先恭喜你了 你已經徹底了解LSP 而我相信大多數的人從來就沒了解過
現在我們已經是專業人士了 讓我們用專業人士的語言來說明 為什麼正方形不能是長方形的subType
Checklist
之前我們要說服一個人說 正方形不能繼承長方形 我們必須寫一段程式 把長方形的物件換成正方形 然後藉由發現錯誤來說服別人
現在我們有了七個準則 我們來看看正方形能不能繼承長方形
1.Covariance of argument: 函式argument一樣 遵守
2.Covariance of result: 函式都回傳void 遵守
3.Exception rule: 都沒有拋出例外 遵守
4.Pre-condition rule: 都沒有Pre-condition 遵守
5.Post-condition rule: 都沒有Post-condition 遵守
6.Invariant rule: 長方形沒有Invariant 正方形的Invariant就是長寬一樣 但在我們正方形的實作中我們有遵守這個Invariant(所以如果我們setWidth只改width 那這條就沒遵守了) 所以這條有遵守
7.Constraint rule: 有趣了 這條沒遵守 因為在Rectangle的Contraint包含了
i. setWidth時不可以動到height
ii. setHeight時不可以動到width
可是我們的正方形違反了這個Constraint
所以 我們可以有信心的說 正方形不是長方形的subType
總結
這可能是S.O.L.I.D裡面最難的一個Principle
要用一句話簡短的總結的話 就是
當你要使用繼承的時候要非常小心 大多數的情況你都不該用繼承 如果你一定要用的話 這裡的七項規則是檢驗的方法 只要你繼承完的類別符合這七項規則 那代表說這個subclass是baseType的subType 那才可以使用繼承
看回我們討論過的Effective Java Item18 你會發現各個大神提倡的概念都是萬變不離其宗