[Engineering] 设计模式奏鸣曲(七):依赖与反向依赖

1. 系统
系统(system)是人们常用的术语,
常见的系统,包括财务系统,计算机系统,血液循环系统,等等,
它为我们描述、理解、划分和分析组织中的现象,提供了有用的框架。
系统的概念,就本质而言是一定环境中一类为达到某种目的,而相互联系、相互作用的事物有机集合体。
——《信息系统开发方法》
系统中各组成要素之间存在着密切的联系,这种联系决定了整个系统的性质。
系统内部的要素本身,也可能构成了一个较小的子系统,
一个系统也可能包含在更大的系统中,这称之为系统的层次性。

计算机科学中的面向对象编程,构建了一个对象系统。
Smalltalk的设计者之一Alan Kay,
在答复Meaning of "Object-Oriented Programming"的邮件中提到了,
I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages.
Alan Kay将对象(object)类比为生物学中的细胞,计算过程通过对象之间交换信息来完成。
2. 依赖
系统中个组成要素之间可能具有依赖关系,
某一部分改变了,其它部分可能会受到影响。
在面向对象的软件系统中,过多的依赖,意味着系统更脆弱。

有两个测量指标,可用于衡量系统中各要素之间的依赖情况,
分别称为传入耦合(afferent coupling)与传出耦合(efferent coupling),
它们反映了系统架构的可维护性问题。
要么对象对其他太多对象负有责任——高传入耦合,
要么这个对象不够独立于其他对象——高传出耦合。
修改一个高传入耦合的对象,是有风险的,依赖该对象的其他对象,都有可能受到影响。
而具有高传出耦合的对象,则更容易受到系统中其它变更的影响。
传入耦合高的对象会造成破坏,传出耦合高的配件则会受到破坏。
我们可以用以下公式,计算系统中每个对象的不稳定性,
其中,1
表示不稳定,0
表示稳定。
不稳定性 = 传出耦合 / (传出耦合 + 传入耦合)
3. 兼容

一般而言,依赖是由自身进行管理的,
但是,自身被哪些对象所依赖,是不可控的。
因此,我们应该尽可能进行兼容更新。
兼容更新:这些更新中会新增接口,所有先前可用的接口仍保持不变。
不兼容更新:这些更新会更改现有接口,使此接口的现有用户操作失败或不正确地执行操作。
—— 接口兼容性
兼容更新,遵循了开闭原则,
即,软件中的对象(类,模块,函数等等),应该对于扩展是开放的,但是对于修改是封闭的。

保持兼容性的另一个办法是,对反向依赖进行管理,
超大型 JavaScript 应用的设计哲学,这篇文章中提到了一些实践,
如果你 enhance 一个模块,你会让这个模块对你产生依赖。
4. 版本
在进行软件开发的时候,我们都希望依赖是稳定的,
如果在开发时,测试时,或者在用户使用时,系统的依赖都是不同的,
那就很难保证功能的稳定性。
除此之外,很多软件系统并不是运行在源代码之上的,
而是运行在“目标代码”之上,
源代码经过编译,构建,或者打包,最终生成了可“执行”的目标代码。
我们期望,不同时期对源代码进行的任一次构建,
都应当生成同样的一份目标代码,至少也应具有相同的功能,
特别是当依赖项是通过网络进行传输的时候。

添加版本号,是一个不错的主意,
但是,每一次升级,都必须让所有用户更新依赖项才会生效,
于是人们提出了语义化版本的概念。
语义化版本是一个约定,
被认为是兼容性的变更,会在下一次构建中自动生效,
而被认为是非兼容性的变更,则需要用户手动更新依赖。
结语
本文讨论了系统中各要素之间的依赖,以及对依赖进行管理所衍生出来的问题,
增加依赖可能会减少重复,但也会让系统变得更脆弱。
为依赖项添加版本号是一个好习惯,
因为不添加意味着总是使用最新的版本。
最后,现阶段进行反向依赖管理仍然是困难的,
这使得我们不得不坚持开闭原则,
改动一个已发布的功能,其影响范围是不可控的。
参考
Compatibility
Malte Ubl - Designing very large JavaScript applications
超大型 JavaScript 应用的设计哲学
信息系统开发方法
持续集成
Semantic Versioning