OOP 13 - Dependency Inversion Principle


Posted by tsungtingdu on 2021-09-30

在上一篇文章當中我們談到開放封閉原則,這裡我們要來談談依賴反轉原則 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 避免受到低層次模組的影響。


鐵人賽發表網址:幫自己搞懂物件導向和設計模式


#OOP #Object-oriented programming #SOLID #TypeScript #2021-ironman







Related Posts

關於

關於

What Type of Laser Engraving Machine Should be Used for Stainless Steel Engraving?

What Type of Laser Engraving Machine Should be Used for Stainless Steel Engraving?

C++ 指標*跟&的用法  (3)

C++ 指標*跟&的用法 (3)


Comments