模块的松紧 — 内聚、耦合与正交
这一章讲三件事: 内聚的七个等级——怎么判断一个模块「纯不纯」; 耦合的六个等级——全局变量为什么是万恶之源;正交性怎么把两把尺推广到 整个系统。 主走查:一个全局变量引发的三模块连环事故,按书里的原始情节走。
1. 两把尺:模块内拉紧,模块间放松
第 04 章把代码分成了模块。这一章回答下一个问题:分得好不好,怎么判断?
软件工程给了两把互补的尺1:
- 内聚度:量一个模块内部的纯粹程度——里面的元素是不是在为同一件事服务。越高越好;
- 耦合度:量模块之间关系的紧密程度——一个模块要不要懂另一个模块的内幕。越低越好。
为什么这两条合起来就等于「好」?作者给的论证是独立性:模块越独立, 复杂度越可控、越可靠、越好维护。而独立性=「模块内元素的关联性达到最强, 模块与模块之间的关联性减到最弱」——内聚管前半句,耦合管后半句2。
内聚塌了的典型症状,书里列了四个,句句能对上日常体感3: 代码难理解、难维护、难复用、一改就碎。
2. 内聚七级:模块的纯度光谱
这一节把内聚度从口号变成刻度。 原书按由劣到优列了七级,每级给一个识别要点4;其中第 4 级行话叫控制耦合,第 5 级叫特征耦合,第 6 级叫数据耦合:
| 等级 | 名字 | 模块里装的是什么 | 识别要点 |
|---|---|---|---|
| 1 | 巧合强度 | 恰巧重复过的几段命令 | 说不出这个模块是干嘛的,没法命名 |
| 2 | 逻辑强度 | 「某一类操作」的抽象集合 | 一个入口,靠参数选择具体功能 |
| 3 | 时间强度 | 「同一时刻要做」的杂事 | 初始化模块:清表、建空间……只因同时发生而聚在一起 |
| 4 | 流程强度 | 某个流程的一段 | 流程图截一段打包;只用其中一个功能时没法用 |
| 5 | 通信强度 | 操作同一份数据的几个功能 | 功能之间靠数据传递相连 |
| 6 | 信息强度 | 同一份数据结构的一组操作 | 多个入口,每个入口一个固定功能 |
| 7 | 功能强度 | 一件事的全部命令 | 「MatrixAdd(x, y)」:整个模块=一个动作 |
走读这张表,会发现分级标准其实只有一条:模块内的东西为什么聚在一起。 「恰巧在同一个文件里」(1 级)、「恰巧同时发生」(3 级)、「 恰巧操作同一数据」(5、6 级)、 「为了同一件事」(7 级)——理由越接近「为了同一件事」,修改越安全。 功能强度模块之所以最优,书里的论证是:所有命令服务于一件事, 「仅修改一部分命令的可能性非常小」——要改,通常是所有使用者都遇到了同一个问题, 改了大家都受益5。
两个细节值得记:
- 逻辑强度有个诱惑:把所有输入输出、所有编辑操作收进一个「大而全」模块,看似整齐。 它的真实风险在接口:几种功能共用一个入口、靠参数区分,「参数的操作方面容易出现编程错误」; 好处也真实:相关功能集中,数据变更时只改这一个模块6。优劣并存,所以要会认;
- 信息强度是实用主义的赢家。书里明说:不必强求 7 级, 6 级(同一数据结构的操作全在一个模块)「也有很高的实用价值」; 迫不得已做低内聚模块时,替代方案的讨论不许省7。
3. 耦合六级:从共享内脏到只递纸条
这一节把耦合度变成刻度,并走全书最著名的事故例:一个全局变量的连环祸。
耦合的分级标准同样只有一条:模块之间靠什么传递影响。由劣到优六级8:
| 等级 | 名字 | 传递什么 | 一句话判词 |
|---|---|---|---|
| 1 | 内容耦合 | 直接引用对方内部、共享命令 | 摸了内脏 |
| 2 | 公共耦合 | 共享全局变量(不属于任何模块、谁都能改的公共数据) | 共用一块没锁的白板 |
| 3 | 外部耦合 | 共享对外声明的数据 | 白板上有名有姓的区域 |
| 4 | 控制耦合 | 传「指令」指挥对方做什么 | 打电话教对方怎么干活 |
| 5 | 特征耦合 | 传一整个数据结构,只用其中一角 | 整箱寄送,只用一件 |
| 6 | 数据耦合 | 只传几个简单值(标量:单个的数或一小段文字) | 递纸条,看完即弃 |
主走查:全局变量 X 的事故链(书里原例)
公共耦合的害处,原书给了一个精确的三模块情节,原样走一遍9:
布局:模块 A、B、C 都使用公共区里的数据 X(一条用户记录)
第 1 步 模块 A 的需求变了:X 的长度要从 10 位改成 12 位
第 2 步 改 A 的程序员动手。他看得见 A 自己的代码;
X 不出现在 A 的任何接口(参数表)上,他无从得知
「还有谁在读 X」——书里的原话:公共数据
「不会出现在模块间的接口上,这使得代码不容易被解读」[^10]
第 3 步 A 改完,自测通过。模块 C 也在读 X,而 C 的程序员
与 A 的程序员互相不知情
第 4 步 C 在某个预想不到的地方炸了。排查开始:
先查 C 自己,再查 B,最后才发现源头在 A 的一次「无关」修改
图说:每一环都是理性决策;事故是结构制造的,不是粗心制造的。
这个链条里没有坏人:改 A 的人尽了责,是结构让他看不见 C。 这就是全局变量的本质问题——它把「谁依赖我」的信息藏了起来, 依赖关系不再出现在任何接口上,只能靠人脑记住,而人脑会忘10。
公共耦合的辩护与反驳
书里诚实记录了反方观点:公共耦合也有省参数的好处—— 不用把数据一个个写进参数表,「现实中确实有不少采用公共耦合的事例」11。 作者的回应分两层12:
- 需要大量参数传递,本身就是模块设计不当的症状——「通过重新设计模块,再次审查数据的位置,参数的数量大多能够减少」;
- 参数多一眼就能看见(代价显性),全局变量的问题会潜伏(代价隐性)。显性的笨拙好过隐性的危险。