在上一篇文章當中,我們提到「單一功能原則」,指每一個類別只會因為一種原因被修改。那麼,如果真的遇到需求變動、需要修改的時候,我們該如何「修改」呢?
SOLID 當中的開放封閉原則 Open-Closed Principle 提供了修改的原則:
In object-oriented programming, the open–closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"; that is, such an entity can allow its behaviour to be extended without modifying its source code.
在物件導向程式設計當中,開放封閉原則指的是,在軟體當中的實體 (不管是 classes, modules, or functions) 應該對於「擴展」是開放的,但對於「修改」是封閉的
好吧,剛剛說到要「修改」,但是這裡又說要對「修改」封閉,到底是什麼意思呢?
先讓我們來看看下面的例子吧
假設今天我們成立一間公司,專門負責計算形狀面積,像是
const calculateArea = (object) => {
return xxx
}
當第一個需求上門的時,我們希望可以處理長方形的面積。因此我們建立了長方形的類別
class Rectangle {
width: number
length: number
constructor(width: number, length: number) {
this.width = width
this.length = length
}
}
接著,更新 calculateArea
方法:
const calculateArea = (object) => {
return object.width * object.length
}
最後成功計算出面積
const rectangle = new Rectangle(13, 17)
calculateArea(rectangle) // 221
當公司業務蒸蒸日上,突然有個新客戶出現,希望我們也能夠計算三角形的面積,這時候我們該怎麼辦呢?
好吧,那麼我們就讓 calculateArea
方法可以根據不同需求來計算面積。因此首先先調整原本的 Rectangle
類別,以及建立新的 Triangle
類別
class Rectangle {
shape: string
width: number
length: number
constructor(width: number, length: number) {
this.width = width
this.length = length
this.shape = 'rectangle'
}
}
class Triangle {
shape: string
base: number
height: number
constructor(base: number, height: number) {
this.base = base
this.height = height
this.shape = 'triangle'
}
}
接著讓我們更新 calculateArea
,讓他能夠根據不同的形狀,回傳正確的面積(如果不是認識的形狀,回傳 null)
const calculateArea = (object) => {
switch (object.shape) {
case 'rectangle':
return object.width * object.length
case 'triangle':
return object.base * object.height / 2
default:
return null
}
}
之後就能夠順利得到結果
const a = new Rectangle(13, 17)
const b = new Triangle(11, 19)
calculateArea(a) // 221
calculateArea(b) // 104.5
後來公司生意越做越大,也開始有更多以前沒有看過的需求進來。每一次有新的需求(形狀)出現,我們就要去修改 calculateArea
方法。另外,在增加新的計算方法的時候,很有可能就不小心碰到原本的方法,進而造成錯誤發生。
這時候有沒有什麼方法可以讓事情變得簡單一點呢?如果我們不要自己為各種形狀建立計算面積的方法,而是讓他們(形狀)自己來告訴我們呢?
讓我們來重新整理一下程式碼。首先,建立一個叫做 Shape
的介面,他要求所有執行這個介面的類別,都需要有一個 getArea
的方法
interface Shape {
getArea(): number
}
接著,讓原本的 Rectangle
和 Triangle
都執行 Shape
介面,並定義好 getArea
,也就是計算面積的方法
class Rectangle implements Shape {
width: number
length: number
constructor(width: number, length: number) {
this.width = width
this.length = length
}
getArea() {
return this.width * this.length
}
}
class Triangle implements Shape {
base: number
height: number
constructor(base: number, height: number) {
this.base = base
this.height = height
}
getArea() {
return this.base * this.height / 2
}
}
接著,calculateArea
方法可以簡化成
const calculateArea = (object) => {
return object.getArea()
}
最後可以得到同樣的結果
const a = new Rectangle(13, 17)
const b = new Triangle(11, 19)
calculateArea(a) // 221
calculateArea(b) // 104.5
在這樣的情況下,不管未來有什麼奇怪的形狀出現,我們只要建立新的類別,執行 Shape
介面,不需要更動 calculateArea
方法了!
從上面的例子來看,我們在做的事情就是,當新類別出現的時候
Shape
介面calculateArea
的修改雖然這裡的 calculateArea
只是一個獨立的方法,但他也可以是某個類別當中的一個方法。也就是說,當需求變動的時候,我們不需要進入這個類別來修改方法。
如果能實現「開放封閉原則」,就代表程式碼具備擴展性和穩定性,不會因為需求而持續修改程式碼,也讓程式碼更容易維護。
當然在真實世界複雜的情況下,可能無法做到純粹的不做任何修改,但如果能夠盡可能降低修改的發生,那麼就能夠降低錯誤發生的可能性。