类别: 原则
类型: 系统设计原则
来源: 罗伯特·C·马丁,C++ Report 工程笔记专栏,1996 年
别名: DIP;依赖倒置原则;依赖抽象而非具体;SOLID 中的 D;Dependency Inversion Principle
类型: 系统设计原则
来源: 罗伯特·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 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 注入、接口注入。那篇文章谈的是对象如何接到邻居。它不是这条原则的换名。接口隔离原则是下一篇 C++ Report 专栏,问的是另一个问题:有了插座之后,它是不是太宽。
核心要点
当政策必须比眼下的机制活得更久时,依赖反转原则才真正值钱。它不是禁止调用具体代码。它是一条规则:重要模块不许提起哪些名字。1
反转的是源码箭头,不是运行时调用
运行时,按钮仍会叫灯打开。改变的是编译期知识。政策不得导入、继承或构造那个细节。细节必须实现政策已经定义好的类型。若高层文件仍写着
OracleDriver,箭头并未反转。你只是多加了一跳。2
高层模块拥有插座
住在供应商包里的接口,是供应商的脸,不是你的。马丁的要点是所有权:
Copy 规定 Reader 和 Writer。键盘和打印机去符合。一所学校若写“必须能跑我们的周六线路”,角色归学校。若写“必须是卫表哥的面包车”,插座就送给表哥了。里氏替换接着问:下一辆车是否真能顶上。3
别让细节从抽象里漏出来
一个返回
OracleResultSet 的 Database 端口,或一份仍要求某公司无线电协议的“公交营运”合同,是插座上还粘着旧插头。调用方仍对着细节编译。把供应商类型、文件格式、个人习惯留在端口后面。单一职责原则不让政策长出额外的变更理由。这条原则不让那些理由被导入。4
不变的东西,不必反转
String、稳定的语言列表、以及会和文件一起死去的一次性脚本,不需要端口。YAGNI适用:第二个机制已经真实存在,或测试必须替换第一个时,再发明插座。每个类一张接口是仪式。代价是导航,外加一种“设计已经很灵活”的错觉。应用场景
当规则应在换工具、换供应商或换人之后仍然成立时,使用依赖反转原则。不要用它去包装你已经信任的每一个对象。在规则和 I/O 之间放一个端口
计费应依赖
Clock 和 Mailer,而不是 System.now 和去年的 SMTP 类。把端口写在政策旁边。把供应商适配在后面。邮件商一换,发票规则不必重开。先雇角色,再把人插进去
周末轮值表若写死一位舅舅的电话,就是焊死的。先写职责:有执照、有保险、周六早上七点。具名的人是一个实现者。替补可以插入,而不必改学校的政策。这是把这条原则用在职务上,而不只是用在类上。
公布公共插座,而不是某个品牌的电器
墙上的插座、USB、以及一份接受“任何持证电工”的市政表格,是同一刀。市民不该为了换水壶而改建房子。若一项公共服务写死某供应商的门户,每一次采购都要重写服务。把插座标准化,让品牌在后面竞争。
测试时用假机制替换真机制
早期代码若直接
new 一个活数据库,在火车上就测不了。依赖一个端口,传入返回已知行的假对象,让政策测试摆脱网络。你不是“在用 Spring”。你是在让重要模块脱离眼下的硬件也能跑。经典案例
依赖反转原则作为公开 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。接线反转了。依赖没有。另一种误用是每个类一张接口,然后一座迷宫里的类型仍会一起变。反转之后,迪米特法则仍然适用:对着插座说话,并不准许你穿过它,伸进供应商的内脏。
常见误区
中文名会撞上“依赖注入”、撞上“每个类都要一张接口”,也撞上好莱坞那句“别打电话给我们,我们会打给你”。依赖反转原则就是依赖注入
依赖反转原则就是依赖注入
注入是交付方法:构造参数、setter,或框架把邻居递给你。反转是依赖规则:那个邻居的类型是政策拥有的抽象,而不是供应商的类。你可以注入一个具体的 Oracle 助手,仍然违反原则。你可以在
main 里构造 KeyboardReader,仍然满足它,因为 Copy 从未提起键盘。依赖反转原则就是每个类都配一张接口
依赖反转原则就是每个类都配一张接口
马丁让
Copy 对着 Reader 和 Writer 反转,因为那些机制预期会变。他并不要求每个整数前面都有一个端口。稳定的语言类型、私有助手、以及只有一次具体生命的模块,赚不到一个插座。唯一实现者永远是它自己的接口,是帽子上的帽子。依赖反转原则就是好莱坞原则
依赖反转原则就是好莱坞原则
“别打电话给我们,我们会打给你”反转的是谁先开口——框架调用你的插件。这条原则反转的是源文件允许提起哪些名字。事件循环、回调和容器可以帮忙。它们也可以去调用具体细节。检验是谁拥有插座,不是框架有没有先响铃。
相关概念
这些页面挨着同一个问题:如何让重要规则不必在每次更换眼下的工具、供应商或人时都被重写。开放封闭原则
新细节应能插入,而不必改政策。这条原则说:插头必须指向政策拥有的抽象。
里氏替换原则
插进去的细节必须兑现插座。一盏不能关的灯,即使实现了
Switchable,也不是替换者。接口隔离原则
有了插座之后,别让调用方依赖自己永远不用的孔。没有隔离的反转,是一根胖插头。
单一职责原则
政策应只有一个变更理由。反转阻止额外理由以具体名字被导入。
关注点分离
把政策与机制分开。这条原则规定两者之间剩下那条依赖的方向。
迪米特法则
只跟眼前的插座说话。不要穿过它伸进供应商的内部对象。