11 DIP: The dependency inversion principle
依赖倒置原则(DIP)
依赖倒置原则(DIP)指出:最灵活的系统是那些源代码依赖仅指向抽象(abstractions)、而非具体实现(concretions)的系统。
在 Java 这类静态类型语言中,这意味着 use、import、include 等声明语句应仅引用包含接口、抽象类或其他抽象声明的源码模块。任何具体实现都不应成为依赖的目标。
这一规则同样适用于 Ruby、Python 这类动态类型语言:源代码依赖不应指向具体模块。不过在这类语言中,“具体模块”的定义稍难界定——具体来说,凡是包含被调用函数实际实现的模块,都属于具体模块。
显然,将这一思想当作“铁律”执行并不现实,因为软件系统必然要依赖大量具体的基础组件。例如 Java 中的 String 类是具体类,强行将其抽象化完全不切实际。对具体的 java.lang.String 的源码依赖,既无法避免,也无需避免。
究其原因,String 类具有极高的稳定性:对它的修改极为罕见,且受严格管控。程序员和架构师无需担心 String 类会出现频繁、随意的变更。
因此,在应用 DIP 时,我们通常会忽略操作系统和平台设施这类稳定的底层依赖。我们容忍这些具体依赖,是因为确信它们不会轻易变更。
我们真正要避免依赖的,是系统中易变的具体元素——也就是那些我们正在积极开发、且频繁变更的模块。
稳定的抽象
对抽象接口的任何修改,必然会传导到其所有具体实现类;反之,对具体实现类的修改,往往(甚至通常)无需改动其实现的接口。因此,接口的易变性远低于实现类。
事实上,优秀的软件设计师和架构师会竭力降低接口的易变性:他们会设法在不修改接口的前提下,为实现类增加功能。这是软件工程最基础的设计准则(Software Design 101)。
由此可推导出结论:稳定的软件架构,应避免依赖易变的具体实现,转而优先使用稳定的抽象接口。这一结论可拆解为一系列具体的编码实践准则:
- 不要引用易变的具体类:应转而引用抽象接口。该规则适用于所有语言(无论静态/动态类型),这也对对象创建方式提出了严格约束——通常要求使用“抽象工厂(Abstract Factories)”模式。
- 不要继承易变的具体类:这是上一条规则的推论,但值得特别强调。在静态类型语言中,继承是所有源码关系中最强、最僵化的一种;因此使用时必须极其谨慎。在动态类型语言中,继承的问题虽相对较小,但它依然是一种依赖——保持谨慎永远是最明智的选择。
- 不要覆写(override)具体函数:具体函数往往会引入源码依赖。覆写这些函数并不能消除依赖(反而会继承这些依赖)。要管理这类依赖,应将函数声明为抽象方法,并为其创建多个实现。
- 绝不提及任何“具体且易变”的元素名称:这其实是对依赖倒置原则本身的重申。
工厂模式(Factories)
为遵守上述规则,创建“易变的具体对象”需要特殊处理。这一谨慎要求的根源在于:几乎所有语言中,创建对象都要求源码依赖该对象的具体定义。
在多数面向对象语言(如 Java)中,我们会使用抽象工厂(Abstract Factory) 模式来管理这种“不受欢迎的依赖”。
图 11.1 展示了该结构:应用程序(Application)通过 Service 接口使用 ConcreteImpl 实现类;但应用程序必须以某种方式创建 ConcreteImpl 的实例。为了在不引入对 ConcreteImpl 源码依赖的前提下实现这一点,应用程序会调用 ServiceFactory 接口的 makeSvc 方法。该方法由 ServiceFactoryImpl 类实现(ServiceFactoryImpl 继承自 ServiceFactory),这个实现类会实例化 ConcreteImpl,并将其以 Service 接口类型返回。
图 11.1 利用抽象工厂模式管理依赖
图 11.1 中的曲线代表一条架构边界:它将“抽象部分”与“具体部分”分隔开。所有源码依赖都跨越这条曲线,且统一指向抽象侧。
这条曲线将系统划分为两个组件:一个是抽象组件,包含应用程序所有高层业务规则;另一个是具体组件,包含这些业务规则所操作的所有实现细节。
注意:控制流的走向与源码依赖的方向相反——源码依赖相对于控制流是“倒置”的,这也是该原则被命名为“依赖倒置”的原因。
具体组件
图 11.1 中的具体组件包含一处依赖,因此它违反了 DIP 原则。这是典型情况:DIP 违规无法完全消除,但可以将其集中到少量具体组件中,并与系统其他部分隔离开。
大多数系统至少会包含一个此类具体组件——通常名为 main(因为它包含 main 函数¹)。在图 11.1 的场景中,main 函数会实例化 ServiceFactoryImpl,并将该实例存入类型为 ServiceFactory 的全局变量;应用程序随后通过这个全局变量访问工厂。
结论
在本书后续内容中,当我们探讨更高层级的架构原则时,依赖倒置原则(DIP)会反复出现。它将成为我们架构图中最核心的组织原则:图 11.1 中的那条曲线,会在后续章节中演变为“架构边界”;依赖跨越这条曲线、单向指向更抽象实体的方式,将成为一条新规则——我们称之为“依赖规则(Dependency Rule)”。

浙公网安备 33010602011771号