SOLID 之 里氏替換原則(Liskov substitution principle)
一樣要考古一下原文:
Subtypes must be substitutable for their base types.
子類別必須要能取代它的父類別。
這次的考古講得非常簡單,它背後所代表的意義是:父類別出現的地方,子類別就能代替它,而且要能做到替換而不出現任何錯誤或異常。
文字描述依然抽象,我們繼續看昨天的例子:
abstract class DataResource |
這裡面,父類別是 DataResource
,子類別是 XmlResource
。現在有個 Model 物件需要把資料拿出來儲存,我們可以這樣寫:
class Model |
但問題來了,昨天我們還有寫另一個 class 它也能取得資料呀!
class JsonResource extends DataResource |
可是 Model 傳入 JsonResource
是不能跑的!因為 Model 只認 XmlResource
,不認 JsonResource
。
$model = new Model(new JsonResource()); |
解決方法其實很簡單,我們只要把 Model 的定義改成兩個子類別所繼承的 DataResource
父類別即可。
class Model |
這程式能跑的原因正是一開始所提到的:「父類別出現的地方,子類別就能代替它」,但有做到「要能做到替換而不出現任何錯誤或異常」嗎。
因為 save()
使用的 getData()
是 DataResource
所定義的公開方法,因為繼承會把父類別的所有行為繼承到子類別,因此子類別也會有 getData()
而不會讓 save()
出錯,因此也有做到「不出現任何錯誤或異常」。
原本程式的做法,是 Model 只能依賴 XmlResource
,這並不符合「里氏替換原則」;改依賴 DataResource
後,程式就符合原則了,接著就會發現程式的擴展性變好,Model 的 Resource 就可以有多樣化選擇,除了 XmlResource
與 JsonResource
之外,甚至還可以新加 CsvResource
讓 Model 也能讀取 CSV 檔。
遵守原則的要領
為避免發生錯誤或異常,實作可以參考要領,如下:
- 子類別必須完全實作父類別的方法
- 子類別可以有屬於自己的屬性和方法
- 覆寫或實作父類別的方法時,輸入參數要與父類別定義的一樣,或是更寬鬆。比方說:父類別是
DataResource
,子類別則可以是XmlResource
或DataResource
- 覆寫或實作父類別的方法時,輸出結果可以縮小。比方說:父類別是
JsonResource
,子類別則可以是JsonResource
或DataResource
優點
里氏替換原則的重點是要增加程式的強健性,讓版本升級的時候也能有很好的兼容性。比方說:子類別增加或修改,並不影響其他子類別,這正是強健性的特質。
上例的使用情況是:子類別處理不同的業務邏輯,參數定義使用父類別,實際上傳遞的是子類別,這樣就能同份定義,執行不同的業務邏輯。