SOLID 里氏替換原則 Liskov Substitution Principle (LSP)

Finn
May 22, 2018

--

子類別必須能夠替換父類別,並且行為正常

在鳥類中實做飛行的方法,身為繼承鳥類的企鵝非常尷尬...

概述

在繼承方面,當子類別替換替換掉父類別時,其功能不受影響

為什麼要遵循呢?

繼承的特性導致高耦合,子類別對於方法修改(Override, Overload) 必須依照父類別行為方向,否則會對整體的繼承體系照成破壞,會有產生不可預測的行為與不好察覺 Bug

如果遵循後可以減少Reuse上的Bug並且擴展子類別以達到新增功能,對於 開放封閉原則 Open-Closed Principle 來說可以安全的新增子類別

範例

寫一個可以計算出矩行面積的方法

所以首先寫了Rectangle 矩行類別,裡面有width跟height

再來建立一個CalculationRectangleArea用area 透過Rectangle來計算面積

然後在test case上面加上計算Rectangle面積來驗證是否正確

測試結果跟預期的一樣,Rectangle面積是長*寬,所以是6

再來新增一個Sqaure正方形,繼承Rectangle

但Sqaure的特性為長寬等長,所以override width跟height

新增sqaure面積的test case

透過測試我們發現測試失敗了

在這個範例中,很明顯地違反LSP,因為Sqaure的等邊長行為所以需要override,違反了Rectangle不變性,導致Sqaure不能替換成Rectangle

如何降低違反LSP情形?

  1. 合約設計 DBC (Design By Contract) : 利用前置條件與後置條件來約束子類別,確保本質不變
  2. 避免繼承 : 盡量利用組合

繼承的目的?

  1. Reuse Code: 可以使用組合,若非得要繼承,就不能override
  2. 為了多型: 可以用抽象類別,避免父類別實作

IS-A是關於行為的

以設計直覺或數學來思考,Square是屬於Rectangle的一種,很自然的會將Square繼承Rectangle

但以設計角度IS-A的關係應該要從 行為 來判斷的,所以Square不是Rectangle物件的行為,這才是軟體該關注的問題,也是開發人員所依賴的

在最前面圖片中企鵝的例子,雖然在生物學上企鵝屬於鳥類,但在鳥類中的飛行行為因為企鵝無法實做導致違反LSP,即使不實做飛行方法,也會有機會誤用,出現無法預測的行為

如果為了解決此問題而修改鳥類,首先鳥類可能在之前已開發完成,這樣修改會造成版本上差異,導致子類別產生新的問題,也就是違反OCP,不應該修改既有的程式碼來達到程式擴充

總結

OCP是OOD主張核心,遵循LSP是幫助遵循OCP的方法之一,正是因為子類別的可替換性,才可以在無需修改情況下得以擴張

對我來說LSP在開發上相較於其他的準則是比較容易違反的,如果以重寫override或重載overload的標準來看,以前我認為的繼承是用在大量重複Code上面來做Reuse,再將不一樣的地方override,但知道LSP後會開始思考使用繼承的目的,如前面所提到,假設你繼承實作並且override的時候,必須先思考修改內容的行為方向是否符合父類別,如果行為不符合就意味著其實一開始就不屬於該父類別

對於DBC來說,有部分語言原生有支援,沒有的可以用單元測試來檢驗與得知知道要遵守哪些約束

但我認為利用組合將共同部分抽取出來,來代替繼承,可以直接避免違反LSP

不過在現實生活中的開發上,解決LSP方法需要考慮成本,這些需要累績經驗做判斷,而非一昧地遵守,最後在書中提到

好的工程師知道何時接受缺陷比追求完美更有利,不過,不應該輕易放棄對 LSP的遵循

--

--