什么是好的代码?好的代码应该模块化。
王垠在其《编程的智慧》中也提到,要“写模块化的代码”。(不对人做评价,这篇文章写得是非常好的。)
如果你读过《代码大全》和《代码整洁之道》等书,一定对“高内聚、低耦合”不陌生。
好的模块化代码就是要高内聚、低耦合。
事实上,内聚和耦合是 1972 年就提出的概念,由于耦合不好具体的衡量,Meilir Page-Jones 在 1992 年提出了共生性(Connascence)。本章重点就是介绍如何评估模块化架构,以及引入共生性这一概念来帮助更好的模块化。
不同的平台、语言为代码提供了不同的复用机制,将相关代码组合成模块。
理解模块对于架构师来说非常重要,因为用来分析架构的工具(可视化等)常常都依赖于模块化的概念。如果一个架构师在设计一个系统时,没有注意到各个部分是如何连接在一起的,那么他们最终创建的系统会带来无数的问题。
架构师必须保持良好的结构,这不会偶然发生。
模块的代码到底是什么?我们用模块化来描述相关代码中的逻辑分组,这些模块可以用来构造一个更复杂的结构。
现代的语言有各种各样的封装机制,例如,许多语言可以在函数/方法、类、包/命名空间中定义行为,每个包都有不同的可见性和范围规则。(这有时候也会让开发人员选择困难)
架构师必须意识到开发者是如何组织包的,如果几个包紧密的耦合在一起,那么重用其中一个包就变得非常困难。
鉴于模块化的重要性,研究人员提供了各种语言无关的标准来衡量,我们专注于三个关键概念:
内聚(Cohesion)
耦合(Coupling)
共生性(Connascence)(注:参考《UML面向对象设计基础》的翻译)
内聚性是指子程序中各种操作之间联系的紧密程度,我们的目标是让每一个模块只做好一件事,不去做其他事情。
试图分割一个内聚的模块只会导致耦合性增加和可读性降低。(Attempting to divide a cohesive module would only result in increased coupling and decreased readability.) —— Larry Constantine

计算机科学家们已经定义了一系列的内聚的衡量标准,从最好到最坏列出如下:
功能性内聚(Functional cohesion):模块内所有元素都为完成同一个功能而存在,共同完成一个单一的功能,模块已不可再分,具有最高的内聚;
顺序内聚(Sequential cohesion):模块必须顺序执行;
通信内聚(Communicational cohesion):两个不同操作的模块使用同样的数据。例如,在数据库中添加一条记录,并根据该信息生成一封邮件;
过程内聚(Procedural cohesion):两个模块必须以特定的次序执行;
时间内聚(Temporal cohesion):把需要同时执行的动作组合在一起形成的模块。
逻辑内聚(Logical cohesion):这种模块把几种相关的功能组合在一起, 每次被调用时,由传送给模块参数来确定该模块应完成哪一种功能。
巧合内聚(Coincidental cohesion):模块内的各个元素之间没有任何联系,只是偶然地被凑到一起;内聚程度最低。

内聚不容易考量,特定的模块需要架构师来具体决定,例如,考虑一个模块定义了:
Customer:
add customer
update customer
get customer
notify customer
get customer orders
cancel customer orders
或者可以说将后两个函数剥离出来,分成两个模块:
Customer:
add customer
update customer
get customer
notify customer
Order:
get customer orders
cancel customer orders
哪个更好?一如既往,这要看情况:
订单只有这两个操作吗?如果是这样,将这些操作放在客户包中维护可能是有意义的;
客户包按预期是否会变得更大?
订单是否需要如此多的客户信息?
这些问题代表了软件架构师工作核心的权衡分析。
由于内聚非常主观,计算机科学家制定了一个标准来衡量内聚性,其中 LCOM(Lack of Cohesion in Methods) 为著名。这里涉及到的数学公式平时很少用到,在此不再展开,只需要知道有这么一个公式,在需要的时候可以再查询拿出来用。想进一步了解的读者可以查看:https://en.wikipedia.org/wiki/Programming_complexity
我们常常谈到要“解耦”,弱耦合是系统可维护的关键。
耦合其实也有多种类型,但在此不再介绍,因为它们已经被共生性(Connascence)所取代。
1996 年 Meilir Page-Jones 发表了
《What Every Programmer Should Know About Object-Oriented Design》,完善了耦合的度量,并命名为:Connascence。
他是这样定义的:
如果一个组件的改变会要求另一个组件进行修改,才能保持系统的整体正确性,那么这两个组件就是共生的。—— Meilir Page-Jones
共生性分为静态的和动态的。我们将分别介绍各种类型的共生性,对于部分重要的、不易理解的,我将补充一些代码案例,作为具体的参考来帮助理解。
静态共生性:
methodA()
改名为 methodB()
时, 调用 methodA()
的地方都要改名,这是代码库中最常见的耦合方式,现代的 IDE 的检索功能使修改代码的名称变得很容易,这是最理想的耦合方式;

如果一个变量从值 100 变成了一个很大的数,变量的类型可能要从 int 改成 BigInteger
例如,在很多语言中,通常会把大于 0 的数字认为是 True,0 认为是 False。下面是 Java 中的一个具体例子:
a.compareTo(b)
// 如果 a = b,则返回值 0;
// 如果 a > b,则返回大于 0 的值;
// 如果 a < b,则返回小于 0 的值。
函数的参数的位置顺序或个数耦合,例如下面的函数增加一个参数后,函数调用将会出错。
针对这个例子,我们可以通过下面的办法,将位置共生性转为名称共生性来降低耦合性:
class User { FirstName, LastName, Address }
void SaveUser(User);
myrepo.SaveUser(new User{
FirstName = "bob",
LastName = "Marley",
Address = "Jamaica"});
多个组件必须就一个特定的算法达成一致。例如:客户端和服务端用相同的算法验证用户身份。这代表一种较高的耦合形式——如果算法细节改变,验证将不再有效。
动态共生性:
代码的执行顺序上的耦合。例如下面的代码,在设置主题之前就发送了,明显在顺序上有问题。
email = new Email();
email.setRecipient("foo@example.com");
email.setSender("me@me.com");
email.send();
email.setSubject("whoops");
常见情况是两个线程同时执行造成的竞赛条件。
这里我们可以看一个有趣的例子,发生在 bootstrap 的一个 issue:https://github.com/twbs/bootstrap/issues/3902
// using bootstrap modal
$(element).modal('hide')
$(element).modal('show') // Error!
// 隐藏一个 modal 大约需要 500ms 的动画,
// 如果你在这时候直接调用了 'show',将会发生异常
// 我们必须这样做
$(element).modal('hide')
$(element).on('hidden.bs.modal', ()=>{
$(element).modal('show') // ok
})
常见的情况在分布式事务中,例如需要在多个独立的数据库中做分布式事务。
两个独立的模块需要共享和更新同一个数据结构,例如:分布式队列。
Page-Jones 指出,共生性有明确的强弱谱系,如下图所示,按强度递增排序。identity 具有最强的共生性,name 具有最弱的共生性。——也就是说用 name 的方式耦合则为最弱的耦合方式。

架构师应该倾向于静态共生性而不是动态共生性,因为开发人员可以通过现代的 IDE 来很快地确定它。
局部性指两个模块的之间的远近程度。
通常情况下,在同一模块中、距离较近的类比在不同模块中、距离距离较远的类具有更高的共生性。换句话说,随着两个模块在代码中的距离增加,共生性会减弱。
共生性的程度与模块的影响大小有关——它影响了几个类还是几十个类?影响较小的共生性对代码库的损坏就较小。
讲了这么多,我们到底如何实践共生性呢?
Page-Jones 提供了三个使用共生性来提高系统模块化的指南:
1.通过将系统拆分成封装的元素,使得整体的共生性达到最弱
2.最大限度地减少任何跨越封装边界的共生性
3.最大限度地提高封装边界的共生性
Jim Weirich (传奇的软件架构创新者,Ruby 社区活跃人士)简化了上面较为抽象的指导,提供了两个更具体的建议:
程度法则(Rule of Degree):将强共生性转化为弱共生性。
局部性规则(Rule of Locality):随着软件元素之间距离的增加,应使用较弱的共生性。
从架构师的角度来看,耦合和共生是有所重叠的,这是不同时代的产物,下图列出两者重叠的部分:

共生性提供了更精细化的考量,例如左边的数据耦合,在右边的静态共生性提供了更具体的建议。
尽管如此,架构师在应用这些指标来分析和设计系统时,存在几个问题:
这些度量从代码层面考察细节,关注代码质量,而不一定是架构。架构师更关注模块如何耦合,而不是耦合程度,例如,架构师关心的是同步或异步通信,而不关心如何实现。
共生性并没有真正解决许多现代架构师必须做出的一个基本决定--在分布式架构(例如:微服务)中,使用同步还是异步通信?在后面会介绍新的方法来思考现代的共生性。
虽然对模块化进行了大量的介绍和思考,开发人员和架构师在实际实施过程中,还是会遇到很多的困难。
纸上得来终觉浅,绝知此事要躬行。
设计良好的架构,并非易事!
欢迎关注我的公众号: