今天來談談 SOLID 當中的里氏替換原則,同樣的先來看一下例子。
延續先前的例子,公司持續拓展,滿足更多不同使用者的需求。現在公司決定,讓使用者可以在建立實例之後,能夠透過一些方法來調整形狀的屬性。以 Rectangle
來說,新增了 setWidth
和 setLength
方法分別調整寬和長。
class Rectangle implements Shape {
width: number
length: number
constructor(width: number, length: number) {
this.width = width
this.length = length
}
setWidth(input: number): void {
this.width = input
}
setLength(input: number): void {
this.length = input
}
getArea(): number {
return this.width * this.length
}
}
所以如果原本建立 rectangle 的時候,設定長和寬為 7 和 7,之後我們可以改為長 11 寬 13,最後會得到面積 143
const rectangle = new Rectangle(7, 7)
rectangle.getArea() // 49
rectangle.setLength(11)
rectangle.setWidth(13)
rectangle.getArea() // 143
這時候部門裡面有人看到上面的例子之後,有個突發奇想,想要讓使用者可以同時修改長和寬。於是就建立了一個繼承 Rectangle
的新類別 NewRectangle
。而因為要能夠實現剛剛的想法,所以也調整了原本 setLength
和 setWidth
的實作方式
class NewRectangle extends Rectangle {
constructor(size: number) {
super(size, size)
}
setWidth(input: number): void {
this.width = this.length = input
}
setLength(input: number): void {
this.width = this.length = input
}
}
結果的確能夠順利實作出這個新的類別,並且同樣能夠計算出面積。
const newRectangle = new NewRectangle(7)
newRectangle.getArea() // 49
這時客戶看到有新的類別,就躍躍欲試的使用,當設定完長 11 寬 13 之後,本來以為會跟 Rectangle
一樣,得到面積 143,但沒想到這時候卻得到結果 169
newRectangle.setLength(11)
newRectangle.setWidth(13)
newRectangle.getArea() // 169
客戶沒有得到期待中的結果,就找公司抱怨說:「為什麼用了你們家新版本的產品,卻沒有得到原本的結果?」
於是公司就回頭跟各部門說:「你們要怎麼更新版本或是改變實作方式都沒關係,但是滿足客戶同樣的期待」
於是,公司內部就出現了一條新規則。
Substitutability is a principle in object-oriented programming stating that, in a computer program, 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.
在物件導向程式設計當中,里氏替換原則指出,如果 S 是 T 的子類別,,那麼 S 就需要能夠取代 T 而不影響其他方面的執行。以剛剛的例子來說,當NewRectangle
繼承了 Rectangle
,那麼就會期待使用者可以用 NewRectangle
來取代 Rectangle
,並看到期待中的結果。
先前提到的「單一功能原則」、「開放封閉原則」、「依賴反轉原則」,談的都是當「外在」需求變動的時候,程式「內部」可以用最少的力氣去修正與擴充,以滿足新的需求。
但隨著時間演進,程式內部可能會有新的實作方式、新的版本出現,但不管內部怎麼調整,對外部的使用者來說,應該都要維持同樣的期待。如果無法維持同樣的期待,那麼我們就需要花額外的力氣去通知使用者調整使用方法或期待。