跳到主要内容

为不可躲的变化而设计 — 每个结构决定都是对未来改动的一笔赌注

这一章讲五件事: 信息隐藏怎么把改动「关」进一个模块(算一笔具体账); 耦合与内聚这对词到底在量什么;为什么「简单」不是审美而是脑子容量问题; 通用与灵活怎么选;以及为什么软件不能像硬件那样用「多装一份」来买可靠性。 全章的预设只有一条,来自第 03/11 章:变化不用等,它一定会来。

1. 先看现象:改一行需求,动五个文件

产品经理走进来:「夜间计费的起始时间,从 23:00 改成 22:00。」一句话的需求。然后:

改动 1 处的版本 改动 5 处的版本
───────────────── ─────────────────
计费模块(规则都在这) 计费模块
改 1 行 订单界面(显示「23点后加价」文案)
完成。 计费服务(判断逻辑)
对账报表(夜间单统计口径)
测试数据(造的都是 23 点后的单)
── 改了 4 处,漏了报表

图说:同一个需求变更,两种结构的代价差 5 倍,还欠一笔「漏改」的债。
本章就讲:第一列的结构是怎么被「设计」出来的,不是碰运气碰出来的。

设计完成后代码怎么摆,决定的就是这张图走哪一列。作者把这件事压成一句: 选择架构、组件和规范技术时,必须以适应重大而持续的变化为前提1

2. 顶层全景:四个下注位

变化必然发生(雷曼定律,第 12 章给数据)


下注① 藏什么:把易变的信息藏进模块深处(信息隐藏/封装)

下注② 连多紧:部件之间少牵连(低耦合),部件内部管好一件事(高内聚)

下注③ 留多宽:通用性(不改就能用于更多场景) vs 灵活性(易改着用于新场景)

下注④ 露多少:模块规格只给使用者必需的信息


兜底:按「一定会出错」设计(不省略分支、故障树)
└─ 极端场合的加价项:冗余(软件版要付双倍开发费)

图说:①~④ 每一注都要付成本(更难设计、更慢),赌的是「未来哪类改动最贵」。

3. 核心原理

3.1 下注①:信息隐藏——把改动关进笼子

信息隐藏:每个模块把一部分信息——数据结构、算法、设计决策,以及接口 (两块软件或软硬件之间约定好的联系方式)——对所有其他模块藏起来,只留一条受控的通路2

它为什么能防住第 1 节那种事故?书里的机制描述是一句因果链,展开是这样2:

信息被藏进模块 A
→ 外面所有模块只知道「问 A 要结果」,不知道 A 内部怎么算
→ 这条信息失效时(算错了,或必须为新需求改掉)
→ 只有 A 需要被检查或修改
→ 改动半径 = A 的边界

图说:隐藏的不是「秘密」,是「会变的东西」。

作者后来给这个概念起的标准做法起了个名字:封装——一组关于「哪些类型的信息该被隐藏」的统一规则。 比如面向对象设计里,把数据(属性)和算法(方法)藏在对象里,外界只能通过调用方法间接影响数据2

回到第 1 节的账:夜间时段藏在计费模块内部 = 改 1 行; 时段判断散布在 5 个文件里 = 改 4 处漏 1 处。下注的方向由此明确:把「最可能变的信息」藏得最深。

顺带一条反方向的教训(原则 80):模块的规格说明要给使用者全部必需信息,且只给必需信息—— 尤其不暴露内部算法和数据结构。因为一旦暴露,总有人「聪明地」直接利用它, 此后你每动一次内部,所有偷看过的模块全部遭殃,书里管这叫级联效应3

3.2 下注②:耦合与内聚——给「牵连」装上仪表

这是设计领域用得最久的一对量词(Constantine 与 Yourdon 在 20 世纪 70 年代定义,作者称它们 至今仍是度量可维护性最好的办法——但见第 5 节,他 2021 年改了口)4:

量的是想要
耦合两个部件之间关联的紧密程度——高耦合=改一个,很可能被迫改一堆
内聚一个部件内部各功能彼此相关的程度——低内聚=出了问题找不到该改哪,加功能不知道动哪

用第 1 节的例子对号:计费逻辑同时出现在订单界面、服务、报表里——这是低内聚(同一件事散在三处); 报表直接读服务内部的数据结构——这是高耦合(服务一改结构,报表跟着断)。 低耦合高内聚的系统,天然长成第 1 节左列的样子。

3.3 简单不是审美,是算力:7±2

原则 67 只有一条心理学依据,却是全书被验证得最狠的一条:一个人很难同时理解超过 7(±2)个事物5。 这是人脑一次能同时端住的东西的上限(数字来自 Miller 1956 年的论文)。它给设计划了硬线: 拆出来的部件,一层里别让人一次端十几个;嵌套别超过几层(第 07 章给编码版的同一原则)。

托尼·霍尔(图灵奖得主)那句话值得整段引出来5:

构建软件设计有两种方法:一种方法是使它简单到明显没有缺陷,另一种方法是使它复杂到没有明显的缺陷

第一种的缺陷少,是因为看得过来——部件少、关系直,人脑真的能把整个系统放进那 7±2 个格子一起检查。 第二种的「没有明显缺陷」翻译过来是:缺陷还在,只是你的脑子再也装不下这个系统,查不动了。

配套信号灯(原则 68):给算法加例外分支时,数一数——特殊案例一多,几乎总是算法本身选错了, 正确动作是退回去重新想,不是继续打补丁6

3.4 下注③:通用性还是灵活性

两个像近义词的词,书里给了精确分工7:

通用性灵活性
定义不做修改就能在不同场景执行预期功能很容易被修改,以在不同场景执行其功能
代价更难设计,通常执行更慢也要额外设计,但运行时更高效
适合同样功能在系统内多处使用;想跨系统复用场景频繁变化;复用时要「改着用」

选哪个,取决于你赌哪类变化:同一件事到处要用(赌「量」),押通用; 同一件事反复变着花样用(赌「变」),押灵活。 拿第 1 节说:计费模块做成「费率全部做成可调的设定」是通用(换城市不改码); 做成「规则引擎里一段好改的配置」是灵活(新规则当天上)。两样都要,就是双倍成本——书里没说这句,但两条原则都写明了各自的代价7

「别重复造轮子」(原则 66)给这件事提供了工程学背书:电子工程师设计新电路板会先翻集成电路目录, 建筑师翻预制门窗目录——查现成件目录,别的领域管这叫「工程」本身; 唯独软件业把这种少见实践单独起了个名字,叫复用8

复用还有个不花钱的起步法(原则 84,「废物利用」):在团队里问一句「谁写过能干 X 的组件」, 找到就适配着用——第 03 章已提,此处只提醒它的设计面:找得到,往往正是因为当年有人多押了一注通用性9

3.5 兜底:按「一定会出错」来设计

原则 76 的立场:不管多努力,软件一定带缺陷上线。设计能优化的只有三件事: 少引入、引入了好检测、遗留的那些不致命或执行时有补偿10。 书里给了三条可执行的手段,第一条最反直觉:

  1. 不要省略分支。 某变量有四个可能值,别只查三个然后「剩下的肯定是第四个」—— 要显式检查第四种,并让错误尽早暴露;
  2. 尽可能多预想「不可能」发生的情况,并写好恢复策略;
  3. 对可预测的危险场景做故障树分析(把「系统怎么才会出灾难」画成一棵「与/或」条件的树,逐条排查)。

再补一条系统间的(原则 85):「错进错出」是不正确的——上游传来的数据非法时, 「它传什么我算什么」会把错误顺流而下污染一整条链。正确做法:程序收到非法输入, 返回一句人能看懂的报错;模块收到非法输入,不继续处理,给上游回一个错误码11。 这样才能尽早拦住错误、保住后面数据的干净。

3.6 加价项:用冗余买可靠性——软件和硬件算盘不同

原则 86 回答一个具体问题:可靠性不够,「多装几份」行不行?12

硬件行,而且划算。 造 2~3 个同样的部件,两种用法:并行(同时各跑各的)运行, 结果不一致就关掉坏的那个;或者冷备,在用的坏了才顶上。

成本只比单份「多一点点」(书里的原话是比两倍稍微多些,设计成本少量增长), 可靠性则按指数——就是翻着倍地往上冲——提升12

软件不行,而且贵。 同一份代码拷两份,坏一起坏——同样的缺陷,两份一模一样。 唯一的软件版做法:给两个不同的设计团队同一份需求规格说明,各自独立设计实现,再并行部署 (行话叫 N 版本程序)。可靠性同样指数级提升,但开发成本直接翻倍—— 而在软件里,设计开发恰恰是主要成本(对照硬件:硬件的大头是制造,所以多造一份只是小钱)。

书里的收束句:软件的超高可靠性是非常昂贵的12。第 02 章那个「每行 1000 美元」的航天飞机软件, 正是这种钱的去处之一。

3.7 作者 2021 年的改口:下注的标的换了

26 年后,作者对这一章动了两处手术,必须分开标13:

  • 耦合与内聚「变得不那么重要,已经被复用所取代」:今天从大量验证过的组件库里选件搭建, 库背后的框架已经替你做到了弱耦合强内聚,「我们不再需要考虑它」;
  • 硬件变快,算法效率的重要性持续下降(原则 79);
  • 反过来,他把原则 76(为出错而设计)和原则 84(随手复用)点名为可能是全部 201 条里最重要的两条, 理由是框架时代让复用无处不在——「我们不再称之为重用,简称其为软件开发」14

4. 作者的判断与证据

说法谁的证据
信息隐藏使软件更易测试维护作者引 Parnas 19722论证(改动半径=模块边界);Parnas 是这篇思想的源头文献
耦合/内聚是最好的可维护性度量作者(1995)4行业共识性论断;2021 年作者亲自降级为「被复用取代」13
人一次端不住 7±2 个事物作者引 Miller5心理学实验(1956)
特例多=算法错作者引 Zerouni6经验法则
硬件冗余划算、软件冗余翻倍作者引 Musa 等12成本结构分析(设计成本占比的差异),非实测账目

判断(我们的,不是书里的): 信息隐藏是一笔双向赌注——把什么藏进笼子, 就赌了「未来的改动不会跨过笼子的边界」。赌赢(改动恰好在笼内),你得到第 1 节左列; 赌输(新需求恰恰要同时动笼内外的信息),封装反而让改动更费劲,因为受控通路要先扩容。 所以下注前最值钱的一问不是「怎么藏」,而是「哪种信息最可能变」——这要求对业务变化方向的判断, 而那个判断任何设计原则都替你做不了。 如果错,会错在: 如果某些系统的变化方向高度稳定(比如接口报文的格式几十年不变), 那么围绕「格式会变」设计的层层封装就是纯开销——判据是:回看这类系统过去五年的变更单,变了什么。

5. 边界与局限

  • 7±2 是「同时」的上限,不是「总共」。 它限制的是同一屏、同一层、同一次评审里出现的事物数; 靠分层(第 05 章 3.4),大系统依然知识可控——每层只端 7±2 个。
  • 「框架已经替你耦合内聚」有前提。 作者 2021 的改口13成立的前提是:你用的库和框架确实质量过硬。 框架选错,耦合问题原样回来,只是换了地方。
  • N 版本程序的实际效果有争议。 书里说「可靠性呈指数级提升」12;后续研究表明独立团队仍会犯 相似的错误(需求本身理解错时,两版一起错),提升幅度常低于理论——本书成书早于这批讨论, 读的时候把「指数级」打预期折扣。
  • 故障树分析只在安全攸关场合才划算。书里引 Leveson 的软件安全文献10; 普通业务系统上全套故障树,花出去的成本会超过它防住的损害。

6. 可带走的

  1. 设计的每个结构决定都是对「未来哪类改动最贵」的下注——下注前先回答:什么最可能变?
  2. 把最可能变的信息藏进模块最深处;模块规格只给必需信息——暴露内部结构等于邀请级联改动;
  3. 耦合量「改一个牵几个」,内聚量「一件事散在几处」;两个都往好处拧,改动半径才小;
  4. 一层里的东西数,以「人一次端得住 7±2 个」为硬线;特例开始变多,是算法选错的警报,不是加分支的信号;
  5. 通用(不改就能多用)和灵活(易改着用)是两个赌注,选一个;都要=双倍成本;
  6. 假设缺陷一定上线:不省略分支、预想「不可能」、危险场景做故障树;
  7. 别接受「错进错出」:非法输入当场拦下,给上游回错误码,保住下游数据的干净;
  8. 「多装一份」在硬件是折扣,在软件是双倍账单——软件的超高可靠性是奢侈品。

7. 原文地图

主题原书章原文位置
信息隐藏/封装(隐藏什么)第4章 设计原则text/16-ch04.txt:57(搜「信息隐藏」)
模块规格只给必需(级联)第4章 设计原则text/16-ch04.txt:239(搜「级联」)
耦合与内聚的定义第4章 设计原则text/16-ch04.txt:141(搜「耦合」)
为变化而设计(六要素)第4章 设计原则text/16-ch04.txt:41(搜「可移植性」)
为维护而设计(制造对照)第4章 设计原则text/16-ch04.txt:175(搜「制造」)
7±2 与霍尔两法第4章 设计原则text/16-ch04.txt:73(搜「7(±2)」) · text/16-ch04.txt:75(搜「简单到明显」)
特殊案例是警报第4章 设计原则text/16-ch04.txt:85(搜「特殊案例」)
通用性第4章 设计原则text/16-ch04.txt:203(搜「通用性」)
灵活性第4章 设计原则text/16-ch04.txt:219(搜「灵活性」)
为出错而设计(分支/故障树)第4章 设计原则text/16-ch04.txt:195(搜「case语句」) · text/16-ch04.txt:199(搜「故障树」)
错进错出不正确第4章 设计原则text/16-ch04.txt:283(搜「错进错出」)
冗余(硬件/软件对比)第4章 设计原则text/16-ch04.txt:299(搜「冷备」) · text/16-ch04.txt:303(搜「翻倍」)
别重复造轮子第4章 设计原则text/16-ch04.txt:65(搜「集成电路」)
2021:耦合内聚被复用取代作者序text/08-fm.txt:41(搜「耦合」)
2021:76/84 最重要作者序text/08-fm.txt:43(搜「84」)

Footnotes

  1. 出处:「第4章 设计原则」第 153 段(text/16-ch04.txt:153,搜「重大和不断的变化」)。模块化、可移植性、可塑性、最小智力距离、知识可控、概念一致六要素同段。

  2. 出处:「第4章 设计原则」第 57 段(text/16-ch04.txt:57,搜「信息隐藏」)。可隐藏的信息清单、隔离错误的机制、封装与面向对象的对应关系同段。 2 3 4

  3. 出处:「第4章 设计原则」第 239 段(text/16-ch04.txt:239,搜「级联」)。

  4. 出处:「第4章 设计原则」第 141 段(text/16-ch04.txt:141,搜「耦合」)。定义、「目前所知……最好的方法」、高耦合与低内聚的后果同段。 2

  5. 出处:「第4章 设计原则」第 73 段(text/16-ch04.txt:73,搜「7(±2)」)。霍尔引语(搜「简单到明显」)同段。 2 3

  6. 出处:「第4章 设计原则」第 85 段(text/16-ch04.txt:85,搜「特殊案例」)。 2

  7. 出处:「第4章 设计原则」第 203 段(text/16-ch04.txt:203,搜「通用性」)与第 219 段(text/16-ch04.txt:219,搜「灵活性」)。 2

  8. 出处:「第4章 设计原则」第 65 段(text/16-ch04.txt:65,搜「集成电路」)。电路板工程师查集成电路目录、建筑师查预制门窗目录、「软件业称这种罕见的实践为『复用』,而不是『工程』」同段。

  9. 出处:「第4章 设计原则」第 279 段(text/16-ch04.txt:279,搜「废物利用」)。

  10. 出处:「第4章 设计原则」第 195 段(text/16-ch04.txt:195,搜「case语句」)。三条手段与故障树分析(搜「故障树」)同段。 2

  11. 出处:「第4章 设计原则」第 283 段(text/16-ch04.txt:283,搜「错进错出」)。多米诺效应与「尽早捕获错误」同段。

  12. 出处:「第4章 设计原则」第 299 段(text/16-ch04.txt:299,搜「冷备」)。并行方式、硬件成本「比两倍稍微多一些」、软件需两团队独立开发、开发成本翻倍与「软件的超高可靠性是非常昂贵的」(搜「翻倍」)同段。 2 3 4 5

  13. 出处:「作者序」第 41 段(text/08-fm.txt:41,搜「耦合」)。原文:「如今耦合和内聚变得不那么重要,已经被重用所取代……我们不再需要考虑它了」。 2 3

  14. 出处:「作者序」第 43 段(text/08-fm.txt:43,搜「84」)。原则 76 与 84「可能是所有原则中最重要的原则」、框架使重用更容易、「不再称之为重用,我们就简称其为软件开发」同段。