《架构整洁之道》之组件耦合

一、无依赖原则

组件依赖关系图不应该出现环。

我们一定有过这样的经历:

当你花了一整天的时间,好不容易搞定了一段代码,第二天上班时却发现这段代码莫名其妙地又不能工作。这通常是因为有人在你走后修改了你所依赖的某个组件。这种情况叫做”一觉醒来综合症”。

这种综合症的主要病因是:

多个程序员同时修改了同一个源代码文件。虽然在规模相对较小、人员较少的项目中,这种问题或许并不严重,但是随着项目的增长,研发人员的增加,这种每天早上刚上班时都要经历一遍的痛苦就会越来越多。甚至会严重到让有的团队在长达数周的时间内都不能发布一个稳定的项目版本,因为每个人都在不停地修改自己的代码,以适应其他人所提交的变更。

针对上述问题逐渐演化出两种解决方案。一种是”每周构建”,另一种是”无依赖环原则”。

1.每周构建

每周构建方案是中小型项目中很常见的一种管理手段。其具体做法如下:在每周的前四天中,让所有的程序员在自己的私有库上工作,忽略其他人的修改,也不考虑互相之间的集成问题;然后在每周五要求所有人将自己所做的变更提交,进行统一构建。

上述方案确实可以让程序员们每周都有四天的时间放手干活。然而一到星期五,所有人都必须要花费大量的精力来处理前四天留下来的问题。

随着项目越来越大,每周五的集成工作会越来越难以按时完成。而随着集成任务越来越重,周六的加班也会变得越来越频繁。经历过几次这样的加班之后,就会有人提出应该将集成任务提前到星期四开始,就这样一步一步地集成工作慢慢地就要占用掉差不多半周的时间。

事实上,这个问题最终还会造成更大的麻烦。因为如果我们想要保持高效率的开发,就不能频繁地进行构建操作,但是如果我们减少了构建的次数,延长了项目被构建的时间间隔,又会影响到该项目的质量,增大它的风险。整个项目会变得越来越难以构建与测试,团队反馈周期会越来越长,研发质量自然也会越来越差。

2.消除循环依赖

对于上述情景,我们的解决办法是将研发项目划分为一些可单独发布的组件,这些组件可以交由单人或者某一组程序员来独立完成。当有人或团队完成某个组件的某个版本时,他们就会通过发布机制通知其他程序员,并给该组件打一个版本号,放入一个共享目录。这样一来,每个人都可以依赖于这些组件公开发布的版本来进行开发,而组件开发者则可以继续去修改自己的私有版本。

每当一个组件发布新版本时,其他依赖这个组件的团队都可以自主决定是否立即采用新版本。若不采用,该团队可以选择继续使用旧版组件,直到他们准备采用新版本为止。

这样就不会出现团队之间相互依赖的情况了。任何一个组件上的变更都不会立刻影响到其他团队。每个团队都可以自主决定是否立即集成自己所依赖组件的新版本。更重要的是,这种方法使我们的集成工作能以一种小型渐进的方式来进行。程序员们再也不需要集中在一起,统一集成相互的变更了。

如你所述,上述整个过程既简单又很符合逻辑,因而得到各个研发团队的广泛采用。但是,如果想要成功推广这个开发流程,就必须控制好组件之间的依赖结构,绝对不能允许结构中存在着循环依赖关系。如果某项目结构中存在着循环依赖关系,那么”一觉醒来综合症”不可避免。

二、自上而下的设计

根据上述讨论,我们可以得出一个无法逃避的结论:组件架构图是不可能自上而下被设计出来的。它必须随着软件系统的变化而变化和扩张,而不可能在系统构建的最初就被完美设计出来。

组件结构图中的一个重要目标是指导如何隔离频繁的变更。我们不希望哪些频繁变更的组件影响到其他本来应该很稳定的组件,例如,我们通常不会希望无关紧要的GUI变更影响到业务逻辑组件;我们也不希望对报表的增删操作影响到其高阶策略。出于这样的考虑,软件架构师们才有必要设计并且铸造出一套组件依赖关系图来,以便将稳定的高价值组件与常变得组件隔离开,从而起到保护作用。

另外,随着应用程序的增长,创建可重用组件的需要也会逐渐重要起来。这时共同复用原则又会开始影响组件的组成。最后当循环依赖出现时,随着无循环依赖原则的应用,组件依赖关系会产生相应的抖动和扩张。

如果我们在设计具体类之前就来设计组件依赖关系,那么几乎是必然要失败的。因为在当下,我们对项目中的共同闭包一无所知,也不可能知道哪些组件可以复用,这样几乎一定会创造出循环依赖的组件。因此,组件依赖关系是必须要随着项目逻辑设计一起扩张和演进的。

三、稳定依赖原则

依赖关系必须要指向更稳定的方向。

设计这件事情不可能是完全静止的,如果我们要让一个设计是可维护性的,那么其中某些部分就必须可变的。

通过共同闭包原则(CCP),我们可以创造出对某些变更敏感,对其他变更不敏感的组件。

任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖,否则这个多变的组件也将会变得非常难以被修改。

1.稳定性

软件组件的变更困难度与很多因素有关,例如代码的体量大小、复杂度、清晰度等。这里我们暂时忽略这些,只集中讨论一个特别因素,那就是让软件组件难以修改的一个最直接的办法就是让很多其他组件依赖于它。带有许多入向依赖关系的组件是非常稳定的,因为它的任何变更都需要应用到所有依赖它的组件上。

2.稳定性指标

  • Fan-in:入向依赖,这个指标指代了组件外部类依赖于组件内部类的数量。
  • Fan-out:出向依赖,这个指标指代了组件内部类依赖于组件外部类的数量。
  • I:不稳定性,I=Fan-out/(Fan-in+Fan-out)。改指标的范围是[0,1],I=0意味着组件是最稳定的,I=1意味着组件是最不稳定的。

当I指标等于1时,说明没有组件依赖当前组件(Fan-in=0),同时该组件却依赖于其他组件(Fan-out>0)。这是组件最不稳定的一种情况,我们认为这种组件是”不负责的、对外依赖的”。由于这个组件没有被其他组件依赖,所以自然也就没有力量会干预它的变更,同时也因为该组件依赖于其他组件,所以就必然会经常需要变更。

相反,当I=0的时候,说明当前组件是其他组件所依赖的目标(Fan-in>0),同时其自身并不依赖任何其他组件(Fan-out=0)。我们通常认为这样的组件是”负责的、不对外依赖的”。这是组件最具稳定性的一种情况,其他组件对它的依赖关系会导致这个组件很难被变更,同时由于它没有对外依赖关系,所以不会有来自外部的变更理由。

稳定依赖原则(SDP)的要求是让每个组件的I指标都必须大于其所依赖组件的I指标。也就是说,组件结构依赖图中各组件的I指标必须要按其依赖关系方向递减。

3.并不是所有组件都应该是最稳定的

如果一个系统中的所有组件都处于最高稳定性状态,那么系统就一定无法再进行变更了,这显然不是我们想要的。事实上,我们设计组件架构图的目的就是要决定让哪些组件最稳定,让哪些组件不稳定。

4.抽象组件

抽象组件通常会非常稳定,可以被那些相对不稳定的组件依赖。

四、稳定抽象原则

一个组件的抽象化程度应该与其稳定性保持一致。

1.高阶策略应该放在哪里

在一个软件系统中,总有些部分是不应该经常发生变更的。这些部分通常用于表现该系统的高阶架构设计及一些策略相关的高阶决策。

如何才能让一个无限稳定的组件(I=0)接受变更呢?
开闭原则(OCP)为我们提供了答案。这个原则告诉我们:创造一个足够灵活、能够被扩展,而且不需要修改的类是可能的,而这正是我们所需要的。

哪一种类符合这个原则呢?
答案是抽象类。

2.稳定抽象原则简介

稳定抽象原则(SAP)为组件的稳定性与它的抽象化程度建立了一种关联。一方面,该原则要求稳定的组件同时应该是抽象的,这样它的稳定性就不会影响到扩展性。另一方面,该原则也要求一个不稳定的组件应该包含具体的实现代码,这样它的不稳定性就可以通过具体的代码被轻易修改。

3.衡量抽象化程度

  • Nc:组件中类的数量。
  • Na:组件中抽象类和接口的数量。
  • A:抽象程度,A=Na/Nc。

A指标的取值范围是从0到1,值为0代表组件中没有任何抽象类,值为1就意味着组件中只有抽象类。

4.主序列

不可能所有的组件都能处于这两个位置上(0,1)和(1,0)组件通常都有各自的稳定程度和抽象化程度。

5.痛苦区

假设某个组件处于(0,0)位置,那么它应该是一个非常稳定但也非常具体的组件。这样的组件在设计上是不佳的,因为它很难被修改,这意味着该组件不能被扩展。这样一来,因为这个组件不是抽象的,而且它又由于稳定性的原因变得特别难以被修改,我们并不希望一个设计良好的组件贴近这个区域,因此(0,0)周围的这个区域被我们称为痛苦区。

6.无用区

靠近(1,1)这一位置点的组件。该位置上的组件不会是我们想要的,因为这些组件通常是无限抽象的,但是没有被其他组件依赖,这样的组件往往无法使用。因此我们将这个区域称为无用区。

7.避开这两个区域

在整条主序列线上,组件所能处于最优的位置是线的两端。一个优秀的软件架构师应该争取将自己设计的大部分组件尽可能地推向这两个位置。然而,以我的个人经验来说,大型系统中的组件不可能做到完全抽象,也不可能做到完全稳定。所以我们只要追求让这些组件位于主序列线上,或者贴近这条线即可。

8.离主序列线的距离

如果让组件位于或者靠近主序列是可取的目标,那么我们就可以创建一个指标来衡量一个组件距离最佳位置的距离。

  • D指标:距离D=|A+I-1|,该指标的取值范围是[0,1]。值为0意味着组件是直接位于主序列线上的,值为1则意味着组件在距离主序列最远的位置。

通过计算每个组件的D指标,就可以量化一个系统设计与主序列的契合程度了。另外,我们也可以用D指标大于0多少来指导组件的重构与重新设计。

文章目录
  1. 一、无依赖原则
    1. 1.每周构建
    2. 2.消除循环依赖
  2. 二、自上而下的设计
  3. 三、稳定依赖原则
    1. 1.稳定性
    2. 2.稳定性指标
    3. 3.并不是所有组件都应该是最稳定的
    4. 4.抽象组件
  4. 四、稳定抽象原则
    1. 1.高阶策略应该放在哪里
    2. 2.稳定抽象原则简介
    3. 3.衡量抽象化程度
    4. 4.主序列
    5. 5.痛苦区
    6. 6.无用区
    7. 7.避开这两个区域
    8. 8.离主序列线的距离