All Articles

直白理解什麼是 Dependency Inversion (依賴反轉)

第一次聽到 Dependency Inversion(依賴反轉) 這個詞,是在面試的時候。AmazingTalker 的 take home 專案裡面提到,期待面試者寫出來的程式碼,符合 SOLID 原則。而其中 SOLID 的 D,指的就是 Dependency Inversion。

使用依賴反轉,可以讓程式碼之間減少依賴,寫出維護成本更低的程式碼。

而這一篇文章希望可以用最直白的方法,讓我們更好理解什麼是依賴反轉。

以下的例子來自於 無瑕的程式碼 : 整潔的軟體設計與架構篇(Clean Architecture: A Craftsman’s Guide to Software Structure and Design) 這本書。

情境

“請寫一支程式,可以輸出 Hello World 字串到終端機上”

如果我們用 C 語言寫會是這樣:

#include<stdio.h>

int main() {
  printf("Hello World");
  return 0;
}

很簡單對吧!

更多情境

  • 如果我今天想要把 Hello World 字串寫到檔案裡面呢?
  • 如果儲存檔案的裝置換成磁帶呢?

我們要每一種裝置都重寫一份 code 嗎? 如果是,那其實我們這支程式依賴於裝置。 當我們有新的裝置要輸出,就要重新修改原本的程式碼。

圖像化表示的話,會是這樣:

依賴反轉範例一

那麼,我們有可能改變這樣的依賴關係嗎?

有的,藉由定義一個彼此溝通合作的介面,我們可以某種形式地反轉它。

在 C 語言裡面,其實定義了 FILE type 的指標,必須要實作五個函數 open, close, read, write, seek。只要有實作五個函數,我們使用 fprintf,就可以使用這個指標作為輸出裝置。

終端機實作了這五個函數,作業系統在串接硬碟時也實作了這五個函數,讓我們可以不論是要輸出到終端機,還是檔案,都可以不用修改原本的程式碼。

舉個實例是這樣:

#include<stdio.h>

// 這邊都不用改
void printHelloWorld(FILE *out) {
  fprintf(out, "Hello World");
}

int main() {
  // 想輸出到終端機
  printHelloWorld(stdout);

  // 想輸出到檔案
  FILE *filePointer = ...;
  printHellowWorld(filePointer);

  // 想輸出到其他裝置
  FILE *deviceFilePointer = ...;
  printHellowWorld(deviceFilePointer);

  return 0;
}

如此一來,我們已經改變了依賴關係,這隻程式實際上不再依賴裝置本身,而是依賴介面,而裝置也依賴介面。也就是說,兩隻程式的關係變成這樣:

依賴反轉範例二

事實上,依賴關係並不是直接反轉過來,而是兩端依賴一個共用的介面。有了介面之後,我們可以讓兩端可以在不修改內部邏輯的前提下,完成不同的任務。

這樣會方便很多,左邊我們可以改成輸出病例、銀行交易紀錄,右邊可以改成終端機、硬碟、磁帶…。

依賴反轉範例三

小小總結

所謂的依賴反轉,就是藉由定義一個溝通的介面,讓兩端的程式碼可以互相合作,以達成不同程式目的,並讓兩端的程式碼不必為了另外一端而做內部修改。

兩端都可以像插件一樣,可以抽換整個模組以達成新的目的,進而達到降低維護成本的目的。

運用 Dependency Inversion 有什麼缺點嗎?

假設我們今天定義出一個很棒的介面,左右兩端各有 10 支程式依賴這個介面,完成 100 個不同的任務。我們用 10+10 的時間完成了原本 100 時間才能完成的任務,聽起來非常棒。

但,如果介面改了呢?

沒錯,就左右兩邊加起來 20 支程式都要改。

商業世界變動快速,我們一定都遇過,業主需求改變、PM 需求改變,老闆今天心情好想加個功能…。 各種因素都有可能造成我們原本定義好的介面必須被改變,而這牽一髮動全身。如果原本每一種 case 都分開寫,還有可能不需要改動到 20 支程式。

所以,如何定義好的介面,讓商業邏輯不會動到它,最有效減少程式碼維護的成本,永遠都是身為一名工程師的修煉。


最後是我個人大推 Clean Architecture 這本書,讓我們用更抽象化的層級思考所寫的程式,那些結構的差異,如何影響我們的開發經驗。以及如何降低維護程式碼的成本,擁有更好的生活。