Skip to main content
類別: 原則
類型: 系統設計原則
來源: 羅伯特·C·馬丁,C++ Report 工程筆記專欄,1996 年
別名: DIP;依賴倒置原則;依賴抽象而非具體;SOLID 中的 D;Dependency Inversion Principle
快速回答依賴反轉原則(Dependency Inversion Principle)是指:高層政策不應依賴底層細節;雙方都應依賴抽象,細節應依賴這些抽象。羅伯特·C·馬丁在 1996 年 C++ Report 專欄裡把它寫下來,針對的是一改印表機就要重寫政策的拷貝程式。實務檢驗很硬:換供應商、換設備或換人,若仍逼你改規則本身,規則就依賴了不該依賴的東西。

什麼是依賴反轉原則?

依賴反轉原則是一條設計規則:政策模組應依賴自己擁有的抽象,而不應依賴眼下碰巧執行這條政策的那個具體機制。
高層模組不應依賴底層模組。雙方都應依賴抽象。抽象不應依賴細節。細節應依賴抽象。
這不是在求你多寫介面。它管的是誰有權逼你重寫。政策是系統存在的理由:拷貝這段、付那筆、錄取這些學生。細節是鍵盤、印表機、Oracle 驅動、以及眼下開車的那位親戚。政策一旦匯入細節,細節每換一次,政策就要重開。 日常圖像是牆上的插座。水壺不是焊在牆皮裡的。房子公布一個插座。水壺、檯燈、手機充電器往上插。若把水壺焊死,加熱管一壞就得拆牆。開放封閉原則想要的是:新水壺進來,不必改房子。這條原則說的是線允許朝哪邊跑:房子不依賴某個品牌的水壺。水壺依賴房子已經定好的插座。 在程式碼裡,一個按名字呼叫 ReadKeyboard()WritePrinter()Copy(),幹的是同一件焊接活。新來一個磁碟寫入器,政策裡就長出一個 if;再來一個設備,再長一個。傷是僵硬:重要模組無法重用,也無法測試,除非把硬體一起拖來。關注點分離拆的是工作。這條原則拆的是原始碼依賴的方向

依賴反轉原則的三層理解

  • 入門:別把燈焊在房子上。先公布插座。然後換一盞燈,不必改佈線圖。
  • 實務:讓政策依賴它自己定義的端口。把供應商、檔案或人放在端口後面。若換品牌仍要改規則,箭頭還是指錯了方向。
  • 進階:執行時仍然從政策流向細節。反轉的是原始碼箭頭。高層模組擁有抽象,細節向內插入。由供應商擁有的包裝,不是反轉。那是舊依賴多了一個名字。

起源

依賴反轉原則起於物件導向 C++ 裡的耦合診斷,不是依賴注入框架的口號。 羅伯特·C·馬丁The Dependency Inversion PrincipleC++ Report 工程筆記專欄的第三篇,1996 年 5 月)裡把它寫下來。他已在 Object Oriented Design Quality Metrics: an analysis of dependencies1994 年)裡主張:原始碼依賴的方向,決定設計會不會變僵。1996 年的專欄給這條規則起了名字。他寫道,傳統的結構化設計,往往讓高層模組依賴底層模組,讓抽象依賴細節。他把修法稱作對這種習慣的反轉 文中的工作傷口,是一台沒有裝置無關 I/O 的機器上的 Copy 程式。政策循環:從鍵盤讀一個字元,寫到印表機。於是 Copy 依賴 ReadKeyboardWritePrinter。加上磁碟檔案,政策就冒出一個布林和一個 if。馬丁的修法是兩個抽象,ReaderWriter,放在 Copy 旁邊,由鍵盤和印表機來實作。依賴反轉了:細節依賴政策的插座。他指出,C 的 stdio.h——getcharputchar——是同一形狀,只是沒有類別。 《敏捷軟體開發:原則、模式與實務》(Prentice Hall,2003 年)重述兩條條款,並補上按鈕/燈的圖景。一個點名 Lamp 的按鈕,以後無法改去開關馬達。一個 ButtonClientSwitchable 插座,讓燈插進來。大約 2004 年Michael Feathers 把馬丁的「前五條」排成 SOLID 口訣。依賴反轉原則是其中的 「D」。 在《架構整潔之道》(Prentice Hall,2017 年)裡,馬丁把同一支箭頭擴成依賴規則:原始碼依賴指向內,指向政策。Martin FowlerInversion of Control Containers and the Dependency Injection Pattern2004 年 1 月 23 日)命名的是一種接線手法——建構函式注入、setter 注入、介面注入。那篇文章談的是物件如何接到鄰居。它不是這條原則的換名。介面隔離原則是下一篇 C++ Report 專欄,問的是另一個問題:有了插座之後,它是不是太寬。

核心要點

當政策必須比眼下的機制活得更久時,依賴反轉原則才真正值錢。它不是禁止呼叫具體程式碼。它是一條規則:重要模組不許提起哪些名字。
1

反轉的是原始碼箭頭,不是執行時呼叫

執行時,按鈕仍會叫燈打開。改變的是編譯期知識。政策不得匯入、繼承或建構那個細節。細節必須實作政策已經定義好的類型。若高層檔案仍寫著 OracleDriver,箭頭並未反轉。你只是多加了一跳。
2

高層模組擁有插座

住在供應商套件裡的介面,是供應商的臉,不是你的。馬丁的要點是所有權:Copy 規定 ReaderWriter。鍵盤和印表機去符合。一所學校若寫「必須能跑我們的週六線路」,角色歸學校。若寫「必須是衛表哥的麵包車」,插座就送給表哥了。里氏替換接著問:下一輛車是否真能頂上。
3

別讓細節從抽象裡漏出來

一個回傳 OracleResultSetDatabase 端口,或一份仍要求某公司無線電協定的「公車營運」合約,是插座上還黏著舊插頭。呼叫方仍對著細節編譯。把供應商類型、檔案格式、個人習慣留在端口後面。單一職責原則不讓政策長出額外的變更理由。這條原則不讓那些理由被匯入
4

不變的東西,不必反轉

String、穩定的語言清單、以及會和檔案一起死去的一次性指令碼,不需要端口。YAGNI適用:第二個機制已經真實存在,或測試必須替換第一個時,再發明插座。每個類別一張介面是儀式。代價是導覽,外加一種「設計已經很靈活」的錯覺。

應用場景

當規則應在換工具、換供應商或換人之後仍然成立時,使用依賴反轉原則。不要用它去包裝你已經信任的每一個物件。

在規則和 I/O 之間放一個端口

計費應依賴 ClockMailer,而不是 System.now 和去年的 SMTP 類別。把端口寫在政策旁邊。把供應商適配在後面。郵件商一換,發票規則不必重開。

先雇角色,再把人插進去

週末輪值表若寫死一位舅舅的電話,就是焊死的。先寫職責:有執照、有保險、週六早上七點。具名的人是一個實作者。替補可以插入,而不必改學校的政策。這是把這條原則用在職務上,而不只是用在類別上。

公布公共插座,而不是某個品牌的電器

牆上的插座、USB、以及一份接受「任何持證電工」的市政表格,是同一刀。市民不該為了換水壺而改建房子。若一項公共服務寫死某供應商的入口網站,每一次採購都要重寫服務。把插座標準化,讓品牌在後面競爭。

測試時用假機制替換真機制

早期程式碼若直接 new 一個活資料庫,在火車上就測不了。依賴一個端口,傳入回傳已知列的假物件,讓政策測試擺脫網路。你不是「在用 Spring」。你是在讓重要模組脫離眼下的硬體也能跑。

經典案例

依賴反轉原則作為公開 API 的帶編號窗口,是 Java 資料庫連接(JDBC)——不是聲稱一個資料庫 API 等於全部架構。 Sun 在 1997 年 1 月 寫出 JDBC 規範,並隨 JDK 1.11997 年 2 月 19 日 出貨。這套 API 故意分成兩層。應用程式碼對著 java.sql.ConnectionStatementResultSet 說話。供應商實作 java.sql.DriverDriverManager.getConnection 查找能處理該 URL 的驅動。於是薪酬模組對著 java.sql 編譯,而不是對著 oracle.jdbc 或某個 MySQL 用戶端。換 jar,留政策。這是馬丁那支反轉箭頭落在平台程式庫裡:高層程式碼和底層驅動都依賴同一批抽象,細節(驅動)依賴這些抽象。 1996 年的 JDBC 草案已經把「給應用作者的 API」和「JDBC 驅動 API」切開。Java SE 62006 年 12 月 11 日)加入 JDBC 4.0 的服務載入:驅動自帶 META-INF/services/java.sql.Driver,平台不必再寫 Class.forName 也能找到它。截至 Java SE 262026 年),java.sql 模組仍匯出這些介面。OpenJDK 的 Connection 類型標著 @since 1.1。這套反轉已經活過了好幾代資料庫。 邊界說明:JDBC 並不讓 SQL 可移植。政策若內嵌 Oracle 外連接語法,即使每個類型都是 java.sql,仍在依賴細節。DriverManager 本身是一個具體的定位器;後來的講授更推薦 DataSource。新設計該抄的是 1997 年政策與驅動的切開,而不是聲稱標準 API 能抹掉方言。若下一套資料庫仍逼你重寫查詢,你反轉了類型,卻把語言焊死了。

邊界與失效場景

當細節既穩定又局部時,依賴反轉原則失效。把 StringMath 或一個單檔指令碼包進 IString,只增加類型,買不來第二個實作。插座是成本。在第二個機制、測試替身或供應商替換已經真實時,再付這筆帳。 當抽象由細節擁有時,它也會失效。驅動套件裡的 IOracleConnection,或一份仍要求某公司附件版式的「標準」表格,會讓政策去追供應商。反轉不是介面的存在。反轉是所有權的方向。 常見誤用是把依賴注入容器當成這條原則。容器可以把一個具體的 OracleRepository 注入一個仍匯入 Oracle 類型的具體 PayrollService。接線反轉了。依賴沒有。另一種誤用是每個類別一張介面,然後一座迷宮裡的類型仍會一起變。反轉之後,迪米特法則仍然適用:對著插座說話,並不准許你穿過它,伸進供應商的內臟。

常見誤區

中文名會撞上「依賴注入」、撞上「每個類別都要一張介面」,也撞上好萊塢那句「別打電話給我們,我們會打給你」。
注入是交付方法:建構參數、setter,或框架把鄰居遞給你。反轉是依賴規則:那個鄰居的類型是政策擁有的抽象,而不是供應商的類別。你可以注入一個具體的 Oracle 助手,仍然違反原則。你可以在 main 裡建構 KeyboardReader,仍然滿足它,因為 Copy 從未提起鍵盤。
馬丁讓 Copy 對著 ReaderWriter 反轉,因為那些機制預期會變。他並不要求每個整數前面都有一個端口。穩定的語言類型、私有助手、以及只有一次具體生命的模組,賺不到一個插座。唯一實作者永遠是它自己的介面,是帽子上的帽子。
「別打電話給我們,我們會打給你」反轉的是誰先開口——框架呼叫你的外掛。這條原則反轉的是原始檔允許提起哪些名字。事件迴圈、回呼和容器可以幫忙。它們也可以去呼叫具體細節。檢驗是誰擁有插座,不是框架有沒有先響鈴。

相關概念

這些頁面挨著同一個問題:如何讓重要規則不必在每次更換眼下的工具、供應商或人時都被重寫。

開放封閉原則

新細節應能插入,而不必改政策。這條原則說:插頭必須指向政策擁有的抽象。

里氏替換原則

插進去的細節必須兌現插座。一盞不能關的燈,即使實作了 Switchable,也不是替換者。

介面隔離原則

有了插座之後,別讓呼叫方依賴自己永遠不用的孔。沒有隔離的反轉,是一根胖插頭。

單一職責原則

政策應只有一個變更理由。反轉阻止額外理由以具體名字被匯入。

關注點分離

把政策與機制分開。這條原則規定兩者之間剩下那條依賴的方向。

迪米特法則

只跟眼前的插座說話。不要穿過它伸進供應商的內部物件。

一句話總結

若換工具、換供應商或換人仍逼你重寫規則,規則就依賴了不該依賴的東西——自己擁有插座,讓細節插進來。