用 TypeScript 簡單介紹 SOLID 原則裡面的 L
此為 SOLID 原則介紹的系列文章之一,所有文章的連結如下。
- 用 TypeScript 解釋 SOLID 原則裡的 S
- 用 PHP 解釋 SOLID 原則裡的 O
- 用 TypeScript 簡單介紹 SOLID 原則裡面的 L
- 用 PHP 簡單介紹 SOLID 原則裡面的 I
- 用 TypeScript 簡單介紹 SOLID 原則裡面的 D
SOLID 原則中的 L
里氏替換原則 (Liskov substitution principle),也就是 SOLID 原則裡面的 L。
由芭芭拉·利斯科夫 (Barbara Liskov) 在一次名為「資料的抽象與層次」的演說中首先提出,後與周以真 (Jeannette Wing) 一同發表論文,正式提出里氏替換原則。
芭芭拉是美國第一位計算機科學女博士,也是 2008 年圖靈獎得主,此乃神人也。
里氏替換原則的說明如下。
if S is a subtype of T, then objects of type T may be replaced with objects of type S .
如果類別 T 有一個子類別 S,那麼 T 可以被 S 替換而不會發生任何錯誤。
Laracasts 的講師的說明如下。
Derived classes must be substitutable for their base classes
衍生的子類別必須可以替換成他們繼承的父類別
里氏替換原則主要是對繼承的規則做出描述,很多時候我們會因為想要重複使用方法而繼承,但是里氏替換原則告訴我們。
繼承不能隨便亂用
因為繼承是很強的依賴關係,會讓兩個類別有著高度的耦合,在子類別中的方法,其行為必須要依據父類別方法的行為去做設計,以避免意想不到的錯誤產生。
舉個長方形與正方形例子。
// 長方形
class Rectangle {
public width: number
public height: number
constructor(width: number, height: number) {
// 檢查長與寬是否都大於 0
if (width <= 0 || height <= 0) {
throw new Error('Width and height must be bigger than 0')
}
this.width = width
this.height = height
}
setWidth(width: number): void {
this.width = width
}
setHeight(height: number): void {
this.height = height
}
area(): number {
return this.width * this.height
}
}
// 正方形
class Square extends Rectangle {
setWidth(width: number): void {
this.width = width
this.height = width
}
setHeight(height: number): void {
this.width = height
this.height = height
}
}
// 寬 + 1
function increaseRectangleWidth(rectangle: Rectangle): void {
rectangle.setWidth(rectangle.width + 1)
}
// 假設我們不知道 Rectangle 與 Square 的內部實際上做了什麼
// 我們以為給了相同的長與寬,面積會是相等的
const rectangleOne = new Rectangle(5, 5)
const squareOne = new Square(5, 5)
// 驗證一下面積是否相等
// true
console.log(rectangleOne.area() === squareOne.area())
// 讓 rectangleOne 與 squareOne 的寬都 + 1
// 假設我們依然以為面積會是相等的
increaseRectangleWidth(rectangleOne)
increaseRectangleWidth(squareOne)
// 因為正方形會強制讓長寬相等,所以面積結果就會出現差異
// false
console.log(rectangleOne.area() === squareOne.area())
正方形屬於長方形的一種,那麼在這裡 Square
類別繼承 Rectangle
類別應該沒有問題吧?但是 Square
類別會做一件 Rectangle
類別不會做的行為。
寬與長會強迫相等
重點就在類別的行為,子類別的行為必須遵循父類別的行為,避免出現意想不到的結果。
但依照這個方式是否符合里氏替換原則,依然有點抽象,所以里氏替換原則大致整理了三個條件。
如果想要成為一個父類別 (Supertype) 的子類別 (Subtype),必須符合這三個條件。
- Preconditions cannot be strengthened in the subtype.
子類別的先決條件不能比父類別更強。 - Postconditions cannot be weakened in the subtype.
子類別的後置條件不能比父類別更弱。 - Invariants must be preserved in the subtype.
子類別必須要保留父類別的不變條件。
先決條件指的是在執行一段程式碼之前,必須要先成立的條件。
例如上述程式碼範例中,如果想要建立一個 Rectangle 實例,長與寬必須都大於 0,否則會拋出錯誤,這就是 Rectangle 類別的前置條件。
class Rectangle {
// ...
constructor(width: number, height: number) {
if (width <= 0 || height <= 0) {
throw new Error('Width and height must be bigger than 0')
}
this.width = width
this.height = height
}
// ...
}
因此繼承 Rectangle 的子類別,其前置條件的長與寬也必須大於 0,但不能增強這個條件,例如多一個長與寬必須相等的條件,這就會違反里氏替換原則。
if (width <= 0 || height <= 0) {
throw new Error('Width and height must be bigger than 0')
}
// 前置條件,子類別不能比父類別更嚴格
if (width !== height) {
throw new Error('Width and height must be equal')
}
後置條件則是相反,指的是執行程式碼過後,必須要成立的條件。
例如長與寬相乘,返回的面積一定大於 0。
area(): number {
return this.width * this.height
}
不變條件是指程式執行過程或部分過程中,始終可被假定成立的條件。
以 Rectangle 類別舉例,長與寬小於 0 會拋出錯誤,不會強迫讓長與寬相等這些條件都是 Rectangle 類別的不變條件。