> ## Documentation Index
> Fetch the complete documentation index at: https://meta.niceshare.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 依賴反轉原則

> 依賴反轉原則是指高層政策不應依賴底層細節，雙方都應依賴抽象。了解其1996年起源、Java JDBC 案例與失效邊界。

<Info>
  **類別**: 原則<br />
  **類型**: 系統設計原則<br />
  **來源**: 羅伯特·C·馬丁，*C++ Report* 工程筆記專欄，1996 年<br />
  **別名**: DIP；依賴倒置原則；依賴抽象而非具體；SOLID 中的 D；Dependency Inversion Principle
</Info>

<Note>
  **快速回答** — **依賴反轉原則**（Dependency Inversion Principle）是指：高層政策不應依賴底層細節；雙方都應依賴抽象，細節應依賴這些抽象。羅伯特·C·馬丁在 1996 年 *C++ Report* 專欄裡把它寫下來，針對的是一改印表機就要重寫政策的拷貝程式。實務檢驗很硬：換供應商、換設備或換人，若仍逼你改規則本身，規則就依賴了不該依賴的東西。
</Note>

## 什麼是依賴反轉原則？

依賴反轉原則是一條設計規則：政策模組應依賴自己擁有的抽象，而不應依賴眼下碰巧執行這條政策的那個具體機制。

> 高層模組不應依賴底層模組。雙方都應依賴抽象。抽象不應依賴細節。細節應依賴抽象。

這不是在求你多寫介面。它管的是*誰有權逼你重寫*。政策是系統存在的理由：拷貝這段、付那筆、錄取這些學生。細節是鍵盤、印表機、Oracle 驅動、以及眼下開車的那位親戚。政策一旦匯入細節，細節每換一次，政策就要重開。

日常圖像是牆上的插座。水壺不是焊在牆皮裡的。房子公布一個插座。水壺、檯燈、手機充電器往上插。若把水壺焊死，加熱管一壞就得拆牆。[開放封閉原則](/zh-hant/principles/open-closed)想要的是：新水壺進來，不必改房子。這條原則說的是*線允許朝哪邊跑*：房子不依賴某個品牌的水壺。水壺依賴房子已經定好的插座。

在程式碼裡，一個按名字呼叫 `ReadKeyboard()` 和 `WritePrinter()` 的 `Copy()`，幹的是同一件焊接活。新來一個磁碟寫入器，政策裡就長出一個 `if`；再來一個設備，再長一個。傷是僵硬：重要模組無法重用，也無法測試，除非把硬體一起拖來。[關注點分離](/zh-hant/principles/separation-of-concerns)拆的是工作。這條原則拆的是*原始碼依賴的方向*。

### 依賴反轉原則的三層理解

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

## 起源

依賴反轉原則起於物件導向 C++ 裡的耦合診斷，不是依賴注入框架的口號。

**羅伯特·C·馬丁**在 *The Dependency Inversion Principle*（*C++ Report* 工程筆記專欄的第三篇，**1996 年 5 月**）裡把它寫下來。他已在 *Object Oriented Design Quality Metrics: an analysis of dependencies*（**1994 年**）裡主張：原始碼依賴的方向，決定設計會不會變僵。1996 年的專欄給這條規則起了名字。他寫道，傳統的結構化設計，往往讓高層模組依賴底層模組，讓抽象依賴細節。他把修法稱作對這種習慣的*反轉*。

文中的工作傷口，是一台沒有裝置無關 I/O 的機器上的 `Copy` 程式。政策循環：從鍵盤讀一個字元，寫到印表機。於是 `Copy` 依賴 `ReadKeyboard` 和 `WritePrinter`。加上磁碟檔案，政策就冒出一個布林和一個 `if`。馬丁的修法是兩個抽象，`Reader` 和 `Writer`，放在 `Copy` 旁邊，由鍵盤和印表機來實作。依賴反轉了：細節依賴政策的插座。他指出，C 的 `stdio.h`——`getchar` 和 `putchar`——是同一形狀，只是沒有類別。

《敏捷軟體開發：原則、模式與實務》（Prentice Hall，**2003 年**）重述兩條條款，並補上按鈕／燈的圖景。一個點名 `Lamp` 的按鈕，以後無法改去開關馬達。一個 `ButtonClient` 或 `Switchable` 插座，讓燈插進來。大約 **2004 年**，**Michael Feathers** 把馬丁的「前五條」排成 SOLID 口訣。依賴反轉原則是其中的 「D」。

在《架構整潔之道》（Prentice Hall，**2017 年**）裡，馬丁把同一支箭頭擴成依賴規則：原始碼依賴指向內，指向政策。**Martin Fowler** 的 *Inversion of Control Containers and the Dependency Injection Pattern*（**2004 年 1 月 23 日**）命名的是一種*接線*手法——建構函式注入、setter 注入、介面注入。那篇文章談的是物件如何*接到*鄰居。它不是這條原則的換名。[介面隔離原則](/zh-hant/principles/interface-segregation)是下一篇 *C++ Report* 專欄，問的是另一個問題：有了插座之後，它是不是太寬。

## 核心要點

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

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

  <Step title="高層模組擁有插座">
    住在供應商套件裡的介面，是供應商的臉，不是你的。馬丁的要點是所有權：`Copy` 規定 `Reader` 和 `Writer`。鍵盤和印表機去*符合*。一所學校若寫「必須能跑我們的週六線路」，角色歸學校。若寫「必須是衛表哥的麵包車」，插座就送給表哥了。[里氏替換](/zh-hant/principles/liskov-substitution)接著問：下一輛車是否真能頂上。
  </Step>

  <Step title="別讓細節從抽象裡漏出來">
    一個回傳 `OracleResultSet` 的 `Database` 端口，或一份仍要求某公司無線電協定的「公車營運」合約，是插座上還黏著舊插頭。呼叫方仍對著細節編譯。把供應商類型、檔案格式、個人習慣留在端口後面。[單一職責原則](/zh-hant/principles/single-responsibility)不讓政策長出額外的變更理由。這條原則不讓那些理由被*匯入*。
  </Step>

  <Step title="不變的東西，不必反轉">
    `String`、穩定的語言清單、以及會和檔案一起死去的一次性指令碼，不需要端口。[YAGNI](/zh-hant/principles/yagni-principle)適用：第二個機制已經真實存在，或測試必須替換第一個時，再發明插座。每個類別一張介面是儀式。代價是導覽，外加一種「設計已經很靈活」的錯覺。
  </Step>
</Steps>

## 應用場景

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

<CardGroup cols={2}>
  <Card title="在規則和 I/O 之間放一個端口">
    計費應依賴 `Clock` 和 `Mailer`，而不是 `System.now` 和去年的 SMTP 類別。把端口寫在政策旁邊。把供應商適配在後面。郵件商一換，發票規則不必重開。
  </Card>

  <Card title="先雇角色，再把人插進去">
    週末輪值表若寫死一位舅舅的電話，就是焊死的。先寫職責：有執照、有保險、週六早上七點。具名的人是一個實作者。替補可以插入，而不必改學校的政策。這是把這條原則用在職務上，而不只是用在類別上。
  </Card>

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

  <Card title="測試時用假機制替換真機制">
    早期程式碼若直接 `new` 一個活資料庫，在火車上就測不了。依賴一個端口，傳入回傳已知列的假物件，讓政策測試擺脫網路。你不是「在用 Spring」。你是在讓重要模組脫離眼下的硬體也能跑。
  </Card>
</CardGroup>

## 經典案例

依賴反轉原則作為公開 API 的帶編號窗口，是 **Java 資料庫連接（JDBC）**——不是聲稱一個資料庫 API 等於全部架構。

Sun 在 **1997 年 1 月** 寫出 JDBC 規範，並隨 **JDK 1.1** 於 **1997 年 2 月 19 日** 出貨。這套 API 故意分成兩層。應用程式碼對著 `java.sql.Connection`、`Statement`、`ResultSet` 說話。供應商實作 `java.sql.Driver`。`DriverManager.getConnection` 查找能處理該 URL 的驅動。於是薪酬模組對著 `java.sql` 編譯，而不是對著 `oracle.jdbc` 或某個 MySQL 用戶端。換 jar，留政策。這是馬丁那支反轉箭頭落在平台程式庫裡：高層程式碼和底層驅動都依賴同一批抽象，細節（驅動）依賴這些抽象。

1996 年的 JDBC 草案已經把「給應用作者的 API」和「JDBC 驅動 API」切開。Java **SE 6**（**2006 年 12 月 11 日**）加入 JDBC 4.0 的服務載入：驅動自帶 `META-INF/services/java.sql.Driver`，平台不必再寫 `Class.forName` 也能找到它。截至 **Java SE 26**（**2026 年**），`java.sql` 模組仍匯出這些介面。OpenJDK 的 `Connection` 類型標著 `@since 1.1`。這套反轉已經活過了好幾代資料庫。

邊界說明：JDBC 並不讓 SQL 可移植。政策若內嵌 Oracle 外連接語法，即使每個類型都是 `java.sql`，仍在依賴細節。`DriverManager` 本身是一個具體的定位器；後來的講授更推薦 `DataSource`。新設計該抄的是 1997 年政策與驅動的切開，而不是聲稱標準 API 能抹掉方言。若下一套資料庫仍逼你重寫查詢，你反轉了類型，卻把語言焊死了。

## 邊界與失效場景

當細節既穩定又局部時，依賴反轉原則失效。把 `String`、`Math` 或一個單檔指令碼包進 `IString`，只增加類型，買不來第二個實作。插座是成本。在第二個機制、測試替身或供應商替換已經真實時，再付這筆帳。

當抽象由細節擁有時，它也會失效。驅動套件裡的 `IOracleConnection`，或一份仍要求某公司附件版式的「標準」表格，會讓政策去追供應商。反轉不是介面的存在。反轉是所有權的方向。

常見誤用是把依賴注入容器當成這條原則。容器可以把一個具體的 `OracleRepository` 注入一個仍匯入 Oracle 類型的具體 `PayrollService`。接線反轉了。依賴沒有。另一種誤用是每個類別一張介面，然後一座迷宮裡的類型仍會一起變。反轉之後，[迪米特法則](/zh-hant/principles/law-of-demeter)仍然適用：對著插座說話，並不准許你穿過它，伸進供應商的內臟。

## 常見誤區

中文名會撞上「依賴注入」、撞上「每個類別都要一張介面」，也撞上好萊塢那句「別打電話給我們，我們會打給你」。

<AccordionGroup>
  <Accordion title="依賴反轉原則就是依賴注入">
    注入是交付方法：建構參數、setter，或框架把鄰居遞給你。反轉是依賴規則：那個鄰居的類型是政策擁有的抽象，而不是供應商的類別。你可以注入一個具體的 Oracle 助手，仍然違反原則。你可以在 `main` 裡建構 `KeyboardReader`，仍然滿足它，因為 `Copy` 從未提起鍵盤。
  </Accordion>

  <Accordion title="依賴反轉原則就是每個類別都配一張介面">
    馬丁讓 `Copy` 對著 `Reader` 和 `Writer` 反轉，因為那些機制預期會變。他並不要求每個整數前面都有一個端口。穩定的語言類型、私有助手、以及只有一次具體生命的模組，賺不到一個插座。唯一實作者永遠是它自己的介面，是帽子上的帽子。
  </Accordion>

  <Accordion title="依賴反轉原則就是好萊塢原則">
    「別打電話給我們，我們會打給你」反轉的是*誰先開口*——框架呼叫你的外掛。這條原則反轉的是*原始檔允許提起哪些名字*。事件迴圈、回呼和容器可以幫忙。它們也可以去呼叫具體細節。檢驗是誰擁有插座，不是框架有沒有先響鈴。
  </Accordion>
</AccordionGroup>

## 相關概念

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

<CardGroup cols={3}>
  <Card title="開放封閉原則" href="/zh-hant/principles/open-closed">新細節應能插入，而不必改政策。這條原則說：插頭必須指向政策擁有的抽象。</Card>
  <Card title="里氏替換原則" href="/zh-hant/principles/liskov-substitution">插進去的細節必須兌現插座。一盞不能關的燈，即使實作了 `Switchable`，也不是替換者。</Card>
  <Card title="介面隔離原則" href="/zh-hant/principles/interface-segregation">有了插座之後，別讓呼叫方依賴自己永遠不用的孔。沒有隔離的反轉，是一根胖插頭。</Card>
  <Card title="單一職責原則" href="/zh-hant/principles/single-responsibility">政策應只有一個變更理由。反轉阻止額外理由以具體名字被匯入。</Card>
  <Card title="關注點分離" href="/zh-hant/principles/separation-of-concerns">把政策與機制分開。這條原則規定兩者之間剩下那條依賴的方向。</Card>
  <Card title="迪米特法則" href="/zh-hant/principles/law-of-demeter">只跟眼前的插座說話。不要穿過它伸進供應商的內部物件。</Card>
</CardGroup>

## 一句話總結

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