> ## 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/principles/open-closed)想要的是：新水壶进来，不必改房子。这条原则说的是*线允许朝哪边跑*：房子不依赖某个品牌的水壶。水壶依赖房子已经定好的插座。

在代码里，一个按名字调用 `ReadKeyboard()` 和 `WritePrinter()` 的 `Copy()`，干的是同一件焊接活。新来一个磁盘写入器，政策里就长出一个 `if`；再来一个设备，再长一个。伤是僵硬：重要模块无法复用，也无法测试，除非把硬件一起拖来。[关注点分离](/zh/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/principles/interface-segregation)是下一篇 *C++ Report* 专栏，问的是另一个问题：有了插座之后，它是不是太宽。

## 核心要点

当政策必须比眼下的机制活得更久时，依赖反转原则才真正值钱。它不是禁止调用具体代码。它是一条规则：重要模块不许提起哪些名字。

<Steps>
  <Step title="反转的是源码箭头，不是运行时调用">
    运行时，按钮仍会叫灯打开。改变的是编译期知识。政策不得导入、继承或构造那个细节。细节必须实现政策已经定义好的类型。若高层文件仍写着 `OracleDriver`，箭头并未反转。你只是多加了一跳。
  </Step>

  <Step title="高层模块拥有插座">
    住在供应商包里的接口，是供应商的脸，不是你的。马丁的要点是所有权：`Copy` 规定 `Reader` 和 `Writer`。键盘和打印机去*符合*。一所学校若写“必须能跑我们的周六线路”，角色归学校。若写“必须是卫表哥的面包车”，插座就送给表哥了。[里氏替换](/zh/principles/liskov-substitution)接着问：下一辆车是否真能顶上。
  </Step>

  <Step title="别让细节从抽象里漏出来">
    一个返回 `OracleResultSet` 的 `Database` 端口，或一份仍要求某公司无线电协议的“公交营运”合同，是插座上还粘着旧插头。调用方仍对着细节编译。把供应商类型、文件格式、个人习惯留在端口后面。[单一职责原则](/zh/principles/single-responsibility)不让政策长出额外的变更理由。这条原则不让那些理由被*导入*。
  </Step>

  <Step title="不变的东西，不必反转">
    `String`、稳定的语言列表、以及会和文件一起死去的一次性脚本，不需要端口。[YAGNI](/zh/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/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/principles/open-closed">新细节应能插入，而不必改政策。这条原则说：插头必须指向政策拥有的抽象。</Card>
  <Card title="里氏替换原则" href="/zh/principles/liskov-substitution">插进去的细节必须兑现插座。一盏不能关的灯，即使实现了 `Switchable`，也不是替换者。</Card>
  <Card title="接口隔离原则" href="/zh/principles/interface-segregation">有了插座之后，别让调用方依赖自己永远不用的孔。没有隔离的反转，是一根胖插头。</Card>
  <Card title="单一职责原则" href="/zh/principles/single-responsibility">政策应只有一个变更理由。反转阻止额外理由以具体名字被导入。</Card>
  <Card title="关注点分离" href="/zh/principles/separation-of-concerns">把政策与机制分开。这条原则规定两者之间剩下那条依赖的方向。</Card>
  <Card title="迪米特法则" href="/zh/principles/law-of-demeter">只跟眼前的插座说话。不要穿过它伸进供应商的内部对象。</Card>
</CardGroup>

## 一句话总结

<Tip>
  若换工具、换供应商或换人仍逼你重写规则，规则就依赖了不该依赖的东西——自己拥有插座，让细节插进来。
</Tip>
