iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0

在上一篇文章當中我們談到開放封閉原則,這裡我們要來談談依賴反轉原則 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 型別,他定義了方法的輸入型別和輸出型別,分別是 Shapenumber

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 介面

所以 ToolcalculateArea 兩者都依賴 AreaCalculator 介面。從高層次模組的角度來看,他只知道要使用長得像是 AreaCalculator 的東西,但是不需要知道實際上會是什麼東西。從低層次模組的角度來看,必須執行 AreaCalculator 介面,也就是說,當中的執行細節,需要滿足這個介面的要求。

小結

「開放封閉原則」讓我們能夠在不修改(或降低修改)的情況下,持續因應變化擴充功能,而根據「依賴反轉原則」,則可以讓程式本身不會因為低層次的模組的改變,而需要修正。

回頭看剛剛的例子,就是 Tool 能夠處理的需求,可以根據傳入的 function 進行功能上的擴充,同時透過 AreaCalculator 的設立,讓Tool 避免受到低層次模組的影響。


上一篇
開放封閉原則 Open-Closed Principle
下一篇
里氏替換原則 Liskov Substitution Principle
系列文
幫自己搞懂物件導向和設計模式30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言