第十一章 依赖倒置原则(DIP)

依赖倒置原则(DIP)告诉我们最灵活的系统应该是其中的源码依赖只指向抽象,而非具体实现。
在静态类型语言,比如Java,用use,import,include语句时,应该只指向源模块,包括接口,抽象类,或其他的抽象声明。而绝不能依赖具体实现类。
在动态语言中运用同样的规则,比如Ruby和Python,源码依赖不该指向具体实现模块。但是,在这类语言中,很难说清具体实现模块怎么定义。具体来说,这种模块就是其中包含了调用具体实现的函数。
很明显,把这种想法当作规则很不现实,因为软件系统肯定得依赖大量得具体实现类库,比如,Java中得String时具体实现的,要把它改成抽象的话不太现实。不能,也不应该避免对具体实现的java.lang.string的源码依赖。
相比较而言,String类非常文档,对这个类的改变非常少,而且可控。程序员和架构师不必单向String类频繁随意的改变。
出于这些原因,我们在考虑DIP时,应该忽略稳定的依赖,比如操作系统,平台工具。我们接纳这些具体实现的依赖是因为我们得知道我们依赖得东西不会改变。

稳定的抽象

每个都抽象接口的改变都放映了对具体实现类的改变。反之,改变具体实现并不总是,或者非常少会要求他们实现的接口改变。因此接口比实现更加不易变。
确实,好的软件设计者和架构师努力去减少接口的易变形,他们得尝试对于实现类增加函数而不改变接口。这是软件设计第一课。
这意味着,稳定得软件架构避免依赖易变得具体类,倾向于使用稳定的抽象接口,可以总结为以下一组非常规范的代码实践:

  • 别引用易变的具体实现类。请引用抽象接口类。无论静态语言还是动态语言,这条规则都适用。这也给对象创建加了约束,进而迫使使用抽象工厂模式创建对象。
  • 别从易变的具体实现类派生。这条是上一条的推导,但这里特别提到。在静态语言中,继承是所有代码关系里最强,最死板的;因此,在使用时应该非常小心。在动态语言中,继承问题较少,但它仍然是个依赖--谨慎总是最明智的选择。
  • 别覆盖具体实现的函数。具体实现的函数常常要求源码依赖。但你覆盖这些函数时,你无法消除这些依赖,事实上,你继承了他们。为了管理这些依赖,这应该把这个函数声明成抽象的,然后可以写多个实现。
  • 别提任何的具体实现和易变类的名字。这只是DIP本身的要求。

工厂

要实现这些规则,易变对象的创建要求有特殊的处理。这种谨慎是必要的,因为在几乎所有的语言中,对象的创建需要在源码上依赖于对象的具体定义。

大多数面向对象语言中,比如Java,我们可以用抽象工厂模式去管理这些不好处理的依赖。

如图11.1展示结构图,Application类通过Service接口用了ConceretImpl类。然而Application类无论怎样都得实例化ConcretImpl类来用。为了不出现源码依赖于ConcreteImpl,Application类可以调用ServiceFactory接口的makeSvc方法,具体实现由继承ServiceFactory接口的ServiceFactoryImpl类提供。这个实现类实例化ConcreteImpl类对象当作Service对象返回。

图11.1 抽象工厂模式管理依赖的应用

图11.1中的曲线时架构的边界。它隔开了抽象和具体实现。所有跨越曲线的源码依赖都是指向抽象这一边的。

这条曲线把系统分成两个组件:一个抽象的,另一个具体实现的,抽象的组件包含所有高层的应用业务规则,具体实现的组件包含业务规则操作的具体实现细节。

控制流的走向和跨越曲线的源码依赖的指向是相反的。源码依赖和控制流是相反的,这也就是我们称该原则为依赖倒置的原因。

具体实现组件

图11.1中的具体实现组件包含简单的依赖,因此违反了DIP。这是很常见的,并不能完全消除违反DIP的地方,但能把他们集中成少量的具体实现组件,以和系统的剩余部分隔离开。

多数系统都会包含至少一个具体实现组件,常常称作"main",因为它包含了main函数。图11.1中main函数将实例化ServiceFactoryImpl类,替换所有类型为ServiceFactory的变量,这样Application类通过这些变量亦即访问了该工厂类对象。

小结

随着我们往下读这本书,逐渐介绍高层的架构原则时,DIP将会反反复复地出现。它将成为我们架构图中最明显地组织原则,类似图11.1中的曲线将成为之后章节的架构边界。跨越这条曲线的方式是单向的,并指向抽象实体,这条新规则不妨称为依赖规则。

results matching ""

    No results matching ""