第一次聽到 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 這本書,讓我們用更抽象化的層級思考所寫的程式,那些結構的差異,如何影響我們的開發經驗。以及如何降低維護程式碼的成本,擁有更好的生活。