在上一篇文章當中我們談到開放封閉原則,這裡我們要來談談依賴反轉原則 Dependency inversion principle。先不談定義,先來看範例。
延續上一篇文章的例子,我們有一間專門負責計算面積的公司,方法 calculateArea
定義如下:
const calculateArea = (object) => {
return object.getArea()
}
由於我們的生意做得太好了,所以有另外一家大公司主動來找我們合作,希望我們加入他們
這家大公司擁有 Tool
類別如下
class Tool {
calculate: Function
constructor(calculateFunction: Function) {
this.calculate = calculateFunction
}
}
只要在建立實例的時候把 calculateArea
方法給傳入,這個 newTool 就成為一個新的、同樣能計算面積的工具了!
const a = new Rectangle(13, 17)
const b = new Triangle(11, 19)
const newTool = new Tool(calculateArea)
newTool.calculate(a) // 221
newTool.calculate(b) // 104.5
於是乎,Tool
公司開始「依賴」著 calculateArea
部門來服務他的客戶。
不過 calculateArea
這個部門也很有自己的想法。有一天突然想到,除了只回傳面積結果之外,如果能夠回傳多一點的細節,譬如計算面積的總成本,也許會更好。所以 calculateArea
就把回傳內容改成下面這樣
const calculateArea = (object) => {
return {
cost: object.getArea() * 0.01,
result: object.getArea()
}
}
結果過沒多久,公司就收到一堆人的抱怨,因為原本大家期待呼叫 Tool
的 calculate 方法會得到面積的數值,然而現在卻得到了一個物件!
newTool.calculate(a) // { cost: 2.21, result: 221 }
newTool.calculate(b) // { cost: 1.045, result: 104.5 }
為了挽救商譽,公司緊急將 Tool
修改成下面這樣
class Tool {
calculateFunction: Function
constructor(calculateFunction) {
this.calculateFunction = calculateFunction
}
calculate(object) {
return this.calculateFunction(object).result
}
}
最後好讓使用者得到同樣的結果
newTool.calculate(a) // 221
newTool.calculate(b) // 104.5
感謝工程師們的努力,公司再次平安度過了一天。然而這家公司還有其他許多的部門,每當這些部門更新或調整各自的方法的時候,Tool
也就會跟著忙著修改,工程師們也就有看起來作不完的事情可以做了。
等等,這好像違反了我們前面提到的「單一功能原則」和「開放封閉原則」,為什麼專門負責計算面積的方法更新,Tool
類別也要跟著修改呢?不能只修改一個地方就好了嗎?
問題發生的原因是,Tool
的實作依賴著 calculateArea
方法,所以在 calculateArea
方法有修改的情況下,如果 Tool
想要維持同樣的產出結果,那麼就必定需要跟著修改。
有沒有什麼方法,可以讓 Tool
不依賴 calculateArea
方法呢?也就是當 calculateArea
方法變動的時候,Tool
類別自己可以完全不用擔心呢?
Tool
類別最終還是得靠 calculateArea
方法來計算出面積,所以不可能拋棄他,不過這次公司學乖了,主動跟個別部門並好規則:
「今天不管你各位怎麼計算面積、系統如何更新,我就是要看到數字,其他的我都不想看到」
講完的同時,公司就提出了一個 AreaCalculator
型別,他定義了方法的輸入型別和輸出型別,分別是 Shape
和 number
type AreaCalculator = (a: Shape) => number;
接著,他繼續規定,要傳入 Tool
的方法,需要遵守AreaCalculator
型別的規定
class Tool {
calculate: AreaCalculator
constructor(calculateFunction: AreaCalculator) {
this.calculate = calculateFunction
}
}
這時候 calculateArea
只好摸摸鼻子,遵守了AreaCalculator
型別的規定,規定輸入的型別是 Shape
而輸出只能是 number
const calculateArea: AreaCalculator = (object: Shape): number => {
return object.getArea()
}
所以未來不管 calculateArea
如何變動,只要遵守著和 Tool
之間的約定 (AreaCalculator
型別),那麼 Tool
就不需要有任何變動。工程師們突然就失業了!
突然之間情勢逆轉,Tool
類別不再依賴著 calculateArea
方法,這就是「依賴反轉」的現象。
所以,究竟什麼是依賴反轉原則呢?
In object-oriented design, the dependency inversion principle is a specific form of loosely coupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details. The principle states:
- High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
在物件導向程式設計當中,依賴反轉原則是一種解耦的形式,根據這個原則執行的時候,高層次的模組 (module) 獨立於低層次模組的執行細節。這個原則指出:
以剛剛的例子為例的話,就是
Tool
類別calculateArea
方法AreaCalculator
介面所以 Tool
和 calculateArea
兩者都依賴 AreaCalculator
介面。從高層次模組的角度來看,他只知道要使用長得像是 AreaCalculator
的東西,但是不需要知道實際上會是什麼東西。從低層次模組的角度來看,必須執行 AreaCalculator
介面,也就是說,當中的執行細節,需要滿足這個介面的要求。
「開放封閉原則」讓我們能夠在不修改(或降低修改)的情況下,持續因應變化擴充功能,而根據「依賴反轉原則」,則可以讓程式本身不會因為低層次的模組的改變,而需要修正。
回頭看剛剛的例子,就是 Tool
能夠處理的需求,可以根據傳入的 function 進行功能上的擴充,同時透過 AreaCalculator
的設立,讓Tool
避免受到低層次模組的影響。