今天我們要說的是第三個原則:里氏替換原則 (LSP)。
很多人以為里氏替換原則只是指導我們如何定義子類別。
LSP 的出處是 Barbara Liskov 的一篇論文:Behavioral Subtyping Using Invariants and Constraints
定義:若對型態 S 的每一個物件 o1,都存在一個型態為 T 的物件 o2,使得在所有針對 T 編寫的程式 P 中,用 o1 替換 o2後,程式 P 的行為功能不變,則 S 是 T 的子型態。
嗯。。。希望你們看得懂,反正我 ╮(╯∀╰)╭。雖然定義看起來很複雜,但這樣的定義我認為可以很準確地表達出 LSP 的思想,即:
子型態必須遵從父型態的行為進行設計。
簡單說,假設我們在寫一個模組 P,而模組 P 裡面有用到 Car 類別的物件 car,而今天我們用 BMW 類別的物件 bmw 來替代 car,而 P 的功能都不會被影響的話,那 BMW 就是 Car 的子類別。
也就是說,只要 S 跟 T 替換後,整個 P 的行為沒有差別,那 S 就是 T 的子型態。從類別上來說, S 完全可以繼承 T,成為T的子類別。
那如果我們在子類 Override 父類呢,這樣的動作還符合 LSP 的原則嗎?
我們可以換個角度來看 LSP 原則。
按照 Design by Contract 設計方法,遵守 LSP 就是遵守以下三個條件:
1.子型態的先決條件 (Preconditions) 不應被加強。
先決條件是指執行一段程式前必須成立的條件。使用者在使用子型態前,要確保子型態的先決條件不會比父型態的更強,但可以削弱。
如一個整數相加功能,輸入的參數必須為 2 個整體並回傳一個整數,且輸入的數字不能小於 0 及大於 50 (先決條件)。
let sum = 0;// a,b 必須 >= 0 && <= 50
function add(int a, int b)
{
result = a + b;
return result;
}sum = add(1,5)
子型態在覆寫這功能時,先決條件不能比父型態強。若父型態輸入的數字要求是「不能小於 0 及大於 50」,子型態輸入的數字則不能是「不能小於 0 及大於 51」,但可以是「不能小於 0 及大於 30」。
2.子型態的後置條件 (Postconditions) 不應被削弱。
後置條件是指執行一段程式後必須成立的條件。使用者在使用子型態後,要確保子型態的後置條件不會比父型態的更弱,但可以加強其後置條件。
let sum = 0;// a,b 必須 >= 0 && <= 50
function add(int a, int b)
{
result = a + b; // 回傳型態必須為 int
return result;
}// result 必須等於 sum
sum = add(1,5)//加強條件
let sum2 = 0;
sum2 = add(1,5)
以相同的例子,這邊的後置條件是回傳的型別必須為 int。即子型態不能回傳非 int 型別,如最後把 int 轉成 String 再回傳。
子型態加強其後置條件,如上例,除了 result 必須等於 sum 外,子型態也可以加強條件,讓 result 也必須等於 sum2。
3. 父型態的不變條件 (Invariants) 必須被子型態所保留。
不變條件指不管在何時何地都不能改變,這是構成整個型態的重要條件。同樣地,子型態必須遵守父型態的不變條件,若然加以修改或不遵守,則會導致多型的重大失敗。
所以,只要 Override 有遵守以上三個原則,他就是符合了 LSP 原則。
試想像一下,如果父類與子類在面對一樣的參數時,子類拋出錯誤,而父類並沒有,或者一個子類有不可預期的副作用等等,這些都是名不符實,沒有真的遵從父類的行為。
以單元測試為例,如果今天寫一個多型的測試,但子類的注入得不到跟父類注入時一樣的結果,單元測試就不會通過,也就表示這樣的子類別不符合 LSP 原則。
其實 LSP 在類別的應用上非常容易明白,但真正難以理解的是要將 LSP 放到軟體架構層來看。
在軟體架構層中,我們會期待被同一群使用者所呼叫的介面都有著一樣的行為。
LSP 是指 T只要能被 S 替代,S 就是 T 的子型態。換句話說,我們不希望在為軟體進行某程度的更新後,行為就變得不一致了。如在專案上,現在有套件 A 更新了, 我們會期待套件的更新不會影響原有程式的運作,而不是更新後一堆東西不能用了。
即使用者只依賴於介面,不需要了解到程式的內部在發生什麼事。今天不管是修 bug、是重構、是用全近的語言來寫,讓版本從 1.0 -> 1.1,我們都是期待一致的行為。如此,就是乎合 LSP 原則的軟體架構。
總結一下,繼承請不要隨意使用。因為繼承是依賴性超強的一個特性,如果稍有一項沒有做對,你的子類就會做出超乎預期的行為,在整個系統已經建構起來後,修改起來會是一程地獄之旅。