软件构造(08):深入理解DIP原则:抽象应属于高层模块

写在前面

在软件构造课程的5-1节:可维护性的度量与构造原则中,我们学习了依赖转置原则(Dependency Inversion Principle)。对于DIP原则的两个方面,“高层模块不应该依赖于低层模块,二者都应该依赖于抽象”这一点比较易于理解,但“抽象不应该依赖于实现细节,实现细节应该依赖于抽象”的含义却比较抽象,以至于笔者在初读讲义时无法理解“依赖转置”中的“转置”体现在何处。
因此笔者查阅了一些资料并写下这篇博客,对于DIP原理的第二部分提供另一种解释:

抽象应该属于高层模块,并由低层模块实现。

下面笔者将说明这个结论与“抽象不应该依赖于实现细节,实现细节应该依赖于抽象”是等价的。

依赖转置原则

首先简单回顾一下依赖转置原则的内容。
A. High level modules should not depend upon low level modules. Both should depend upon abstractions.
高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
B. Abstractions should not depend upon details. Details should depend upon abstractions“, i.e., use lots of interfaces and abstractions.
抽象不应该依赖于实现细节,实现细节应该依赖于抽象。

下面为了讲清楚“转置”体现在何处,笔者把讲义上的图片修改了一下用于讲解。

第一部分:依赖抽象

DIP原则的第一点比较简单,用讲义上的图片即可说明。

第一种是最差劲的情况,高层模块直接依赖于低层模块的服务,对低层模块实现细节的改动将迫使高层模块依次做出改动。在这里插入图片描述
当只实现第一部分:“依赖抽象”,但不实现第二部分“抽象属于高层模块”时,一种常见的做法是将接口与其实现类放在同一个表示低层抽象的包内,则模块之间的依赖关系如下(图中圆角矩形表示Java中的包或C++中的namespace):
在这里插入图片描述
这里由于接口不属于高层模块而是属于低层模块,因此接口和实现类被放在同一个包内,即“抽象依赖于实现细节”,我们下一步要做的就是让“实现细节依赖于抽象”。

事实上,这么说实在抽象,就算直接看Robert的英文原句,仍然让人觉得这个表述很糟糕,笔者花了好半天才搞明白它的意思。如果读者看到这里还不明白“抽象不应该依赖于实现细节,实现细节应该依赖于抽象”这句话的含义也没有关系,在下一小节中我们将结合该节内容做进一步的阐述。

因此这里笔者将为DIP原则的第二部分给出一个更加容易理解的说法:

抽象应该属于高层模块,并由低层模块实现。
具体到接口层次,就是:
接口应该属于客户,而不属于其实现。

第二部分:抽象属于高层模块

第二部分比较难以理解。讲义上没有明确说明“转置”体现在何处,实际上“转置”的部分就存在于DIP原则的第二部分中,将接口的所有权与模块之间的依赖关系“倒置”了,让接口属于高层模块,而非接口的实现类。

在上面的图片中,我们看到当抽象不属于高层模块时,高层模块中的高级策略依赖于低层模块,应用程序的高层策略尚未与低层实现分开,抽象也没有与细节分开。

下面我们实现DIP的第二部分,令抽象属于高层模块,并由低层模块实现。在Java中,这种“属于”的关系是通过包实现的,具体的方法就是将为高层模块提供服务的接口和高层模块放在同一个包内,将该接口的具体实现类放在另一个包内,如下图所示:
在这里插入图片描述
可以看到,这里接口的所有权发生了“倒置”,从低层模块所有变成高层模块所有;模块之间的依赖关系也发生了“倒置”,原来是高层模块中的高级策略依赖于低层模块,现在图中两个箭头发生了逆转,低层模块依赖于高层模块,为高层模块提供服务。也就是著名的Hollywood原则:“Don’t call us, we’ll call you”。

现在我们可以解释一下为什么我们给出的直观解释与Robert的原话等价。
在现在的实现中,接口的实现类是根据更高级别的组件的需求定义的(细节依赖于抽象);而不是更低级别的组件的行为定义接口,然后强迫高层模块去调用它们(抽象依赖于实现细节)。

接口与客户端组件的这种关联从逻辑上颠倒了传统的的依赖流,下面我们来看看这样做有什么好处。

为什么抽象应该属于高层模块

更简单的高层模块

高层代码包含了一个应用程序中的重要策略选择和业务模型,正是这些高层模块才使得其所在的应用程序区别于其他应用程序。

因此高层模块的代码具有最高的价值,我们的目标之一就是保持高层模块尽可能干净,从而使之更易于复用。让高层模块拥有接口所有权使我们能够设计最简洁的接口来实现此目标。通过这种方式,我们可以避免底层模块改动迫使高层模块进行调整以适应较低级别的细节的情况。

更好的抽象和接口

高层模块定义接口的优点

通过让高层模块拥有接口所有权,接口和抽象本身也变得更好:

  • 最首要的一点,就是对低层模块的任何改动都不会再影响到高层模块。

  • 这些接口离高层模块中的业务代码更近,它们不是由接口的实现者定义,而是由客户端定义的,每个较高层次都为它所需要的服务声明一个抽象接口。如果客户端更改,则高层模块中的接口也会更改。这意味着接口和客户端在概念上比接口和实现者更紧密相关,让高层模块拥有接口所有权可以保证较好的抽象一致性。

  • 根据上面的论述,让高层模块拥有接口所有权也符合SLOID中的另一条原则:接口隔离原则,也就是“客户端不应依赖于它们不需要的方法”。我们可以将其解释为“接口是由客户的需求定义的”,这一点是很清楚的,这里不再赘述。

  • 便于重用高层的策略设置模块。让高层模块拥有接口所有权可以使高层模块不必在每次需要调用接口时都与低层模块进行交互,从而独立于低层模块,使高层模块更易于重用。这也是框架设计的核心原则之一。

接口实现者定义接口的缺点

为什么不应当让接口的实现者定义接口呢?这里笔者将尝试从“实现者定义的接口”行为背后的逻辑进行论述。

如果接口是由实现者定义的,则很容易泄漏该特定实现者的API。这将导致以后很难为接口提供不同的实现,而如果无法为接口提供不同的实现,定义接口就显得毫无意义了。

改善整体架构

在更大的层次上,通过让高层模块拥有接口所有权可以使代码的组织趋于改善,这使得这种方法在某种程度上成为以顶层抽象为核心的架构的基础。比如SOLID的提出者Robert前几年提出的Clean Architecture,使这个体系结构起作用的首要规则是“依赖关系规则”,而该规则在组件边界处的具体实现正是依靠让高层模块拥有接口的所有权。这种依赖接口,而不是依赖实现的方式保证跨越组件边界的依赖方向永远与控制流的方向相反,该原则指导我们设计组件间依赖的方向,进而设计一个好的架构。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值