跳到主要内容

维护的真相 — 软件在腐败,除非你管住变化

这一章讲四件事: 雷曼四定律——软件为什么注定被改、为什么越改越糟、改动量为什么必须平稳、 以及为什么连「完美的需求」也拦不住演变;治症状不治病的经典反例(主走查); 维护引入错误的机制与「这次很简单」的陷阱;翻新与重写的选择法。这是全书终点: 前十一章防的每一件事,都会随时间再发生一遍。

1. 先看现象:三年的系统,没人敢动

一个运行三年的系统,问它的维护团队:

  • 「这个模块为什么这么写?」——「不知道,前年改的,当时赶上线。」
  • 「这里看起来是个 bug,能顺手修了吗?」——「别!上次有人顺手修,炸了三个下游。」
  • 「这版能多加十个功能吗?」——「上次一次加了十一个,那个版本修了一个月。」

三个回答对应三条规律,书里都给了正式名字。维护(修改软件以满足新功能、跑得更省、修掉错误的工作) 不是开发的收尾杂活——它才是软件一生中持续时间最长的状态1

2. 顶层全景

系统上线
│ 定律一:会被持续修改(使用催生新需求)

每次修改 = 一次打乱
│ 定律二:熵增加——越改越复杂、越脆弱

对抗手段一:管住每次改动的质量
│ 没坏别修 · 治病不治症状 · 别信「这次很简单」 · 改完回归测试

对抗手段二:管住改动的「量」
│ 定律三:熟悉守恒——版本间改动量忽大忽小,质量就崩

挡不住的那部分:定点翻新(先翻最差的)/ 有时重写
│ 定律四:系统存在本身催生新问题——演变没有终点

直到重写比维护更划算,循环重启(定律一的另一面)

图说:四个定律卡在流程的四个位置:一管「会不会停」,二管「为什么变糟」,
三管「节奏」,四管「终点」。

3. 核心原理

3.1 雷曼四定律:软件的时间物理

Manny Lehman(第 01 章就出场的软件演化研究者)给大型程序总结的规律,书里引了四条2:

定律一:持续变化。 正在使用的大型系统会被不断修改——因为使用会催生新功能—— 一直改到从头重写变得更划算为止3

定律二:软件越改越乱。 热力学里量「混乱程度」的那个词叫(孤立系统会自发地从有序走向无序); 把它用到软件上,持续变化让系统越来越复杂、越来越无序;变化导致不稳定, 所以所有有用的软件都在向更低的可靠性、更差的可维护性漂移4。 每次改动都在结构上留一道疤,疤多了,没人能整体看懂它。

定律三:熟悉守恒。 发布版本之间的改动量,必须相对稳定。书里的数据形态: 某个版本的变化量明显高于平均水平,它就会「性能差,可靠性差,故障率高,成本时间都超支」; 偏离平均越多,风险越大。机制有两层:一是稳定效应——改动本身引发不稳定,大量改动攒出的不稳定, 不是一个发版动作能吸收的;二是心理层——开发者对产品的熟悉感随时间衰减, 版本间隔越久、改动越多,就越是在对「陌生代码」动刀5

定律四:存在促进演变。 最深的一条:就算你提前写出了「完美」的需求、开发中一个字没改、 系统精确满足需求——演变照样发生。因为把系统引入它要解决的环境,这个动作本身改变了环境, 环境一变,新问题就冒出来6

四条合起来的姿态:演变不是管理失误的产物,是软件的存在方式。 管理能选的不是「改不改」,而是「怎么改、多快改、多大步改」。

3.2 主走查:那个「除以 2」的反例

原则 188 的例子是「治症状不治病」的教科书演示,值得整段走完7:

现象: 你在排查一个故障,发现某组件每次传出的值,正好是期望值的 2 倍

解法一(发送端打补丁): 在它传出之前把值除以 2。系统立刻「正常」了。 书里判定它不合适,两条理由:一,「总是 2 倍」未必对所有情况成立——某个分支里可能是 3 倍, 补丁一盖,错得更大;二,程序里从此存在两个互相抵消的错误——源头错着,补丁错着, 合起来看着对。后来的人读懂这两处任何一处,都会「修复」它,系统当场崩掉。 书里的判词:这会让程序在未来实际上无法被维护7

解法二(更糟): 不动原组件,让接收方把收到的值除以 2。 上面两条毛病全在,还多一条:未来所有调用这个组件的新代码,拿到的都是没除过的值—— 每个新调用者都要自己再除一次,而没有人知道要除7

正解: 查程序,弄清「为什么值总是翻倍」,从源头修7

配套的兄弟原则(原则 187): 你在检查代码时「发现」一个疑似 bug——别顺手修。 很可能你修进去的是新错误;正确动作是记录并提交变更请求,让配置控制(第 11 章 3.2) 和技术评审去判定它是不是 bug、优先级多高8

两条合起来,就是维护的第一戒律:改动的价值不在「改完看起来正常」,在「改动之后系统仍然说得清」。

3.3 维护在制造错误:20%~50% 与「这次很简单」

原则 195 的数字反直觉到该背下来:维护期间的修改引入的错误,远多于最初的开发阶段; 维护团队报告,20%~50% 的改动会引入新的错误9。 (你以为是「维护=小修小补」,实际是「每次改动都是一次手术,手术有并发症率」。)

为什么这么高?原则 197 给了机制——温伯格的自我失效模型10:

「这次改动很简单/显而易见」的念头
→ 开发者放松警惕
→ 跳过那些保证质量的手段(评审、核查、回归)
→ 大概率执行一次不正确的改动
→ 表现为一个错误的变更,或一个意想不到的副作用

图说:念头本身参与了制造错误——这就是「自我失效」四个字的含义:
相信它简单,恰恰使它容易出错。

所以书里给的三道护栏,件件对着「放松警惕」:变更经过核准(第 11 章)、 每项变更做核查(第 07 章的手动执行)、每组变更后回归测试10

回归测试(原则 196)值得单独说清:改动后,对所有先前已测试过的功能再测一遍。 维护改动分三种——改正性(修缺陷)、适应性(适配新环境)、完善性(提性能)—— 无论哪种,你都要验证改动本身生效,更要验证那些原本正常的功能现在还正常。 大多数人跳过回归,理由是「我的改动没有影响」——恰好是 3.3 的那个念头11

3.4 老程序为什么越养越难,以及翻新的选择法

越老越难养(原则 191):每次修改都要动系统里的一些组件;程序越「老」, 单次改动需要波及的组件比例越高——因为历次改动已经把结构改坏(定律二), 改动像在打结的毛线团里抽线,一抽满团动12

于是面临选择。书里给了一套排序:

  • 先翻新最差的(原则 194):「最差」的定义精确——消耗改正性维护费用最多的组件。 书里引温伯格的案例:重写一个 800 行的模块(它占全部改正性维护成本的 30%), 就能为整体维护省下大量资源——按维护账单排优先级,不按代码审美13;
  • 有时重写更好(原则 193):如今「重建/翻新/逆向工程」的讨论让人以为翻新很容易;其实很难。 判断题书里给得很锋利:如果你补做了设计文档,维护者真的会用吗?——不会的话, 翻新就是在为不存在的读者花钱,从头重写反而干净14;
  • 机械结构化没用(原则 198):把只会乱跳转的老代码「机械地」转成规范写法,不会变好—— 通常同样糟糕。要做就重新考虑模块划分、从头重新设计15;
  • 动手前先测量(原则 199):要优化(让程序更快的改动),记住二八律——80% 的 CPU 周期耗在 20% 的代码上; 用性能剖析工具(profiler:盯着程序跑一遍、统计时间花在哪的工具)找到那 20% 的热点,只优化它16

语言也在这本账里(原则 192):APL、BASIC、LISP 一类利于快速开发,本质上难维护; Ada、Pascal 反之;强制高内聚低耦合的语言(如 Eiffel)两头都帮17

3.5 需求先改,以及终局

补一条流程序(原则 189):各方同意增强软件后,第一件事是更新需求规格说明并批准, 然后才动设计和代码——否则客户、市场、开发三方对「到底改什么」各持一版, 改完必吵18。这是第 11 章的变更控制链在维护场景的第一环。

而终局(定律一的另一面):当维护成本追上重写成本,系统被替换——然后新系统开始它自己的演变循环3。 书里第 9 章 201 条到此收官;作者 2021 年复审时的评语只有一句: 「对于第 9 章中介绍的演变原则,全部依然有效。」——全书 9 章里唯一一组 26 年后一条不改、没有附注的19

4. 作者的判断与证据

说法谁的证据
持续变化/熵增加/熟悉守恒/存在促进演变作者引 Lehman3456大型系统演化的实证规律(Lehman 数十年研究,书内给原始论文)
维护引入 20%~50% 新错误作者引 Humphrey9维护团队报告的数据区间
「总是 2 倍」反例作者引 McConnell7教学反例(演示性的具体场景)
800 行模块占 30% 改正性维护成本作者引温伯格案例13单案例;量级非统计
发布前错误多→发布后错误多作者引 Dunn20经验数据支撑(「被充分支持的」是作者原话)

判断(我们的,不是书里的): 雷曼四定律在云服务时代不但成立,还加速了—— 持续部署把「发版」的间隔从月压到天,定律三的「改动量平稳」从「每个版本」细化到了「每次部署」。 今天读这组定律,该把「版本」翻译成「部署」:一次大上线(几十个功能一起发)正是「高于平均水平的变化量」, 而金丝雀发布、灰度放量,本质是把一次大变化拆成一串小变化来喂给系统。 如果错,会错在: 如果某类系统的正确性只能靠「大爆炸式切换」保证(比如固有的强一致升级), 拆分发布就不可行,定律三的对策要退回「拉长回归验证时间」——判据是系统能不能容忍新旧两版并存。

5. 边界与局限

  • 雷曼定律出自「大型程序」的观察(书里引的原始论文标题就带 Large Program); 小工具、一次性脚本可以不适用——它们活不到熵增 visible 的年纪4
  • 「20%~50%」是当年维护团队的自报区间9,不同类型的系统差异大; 但「维护引入错误的率不低于开发」这个方向,后续研究一直支持。
  • 「先翻新最差」的「最差」必须是维护账单定义的。按「代码难看」排的翻新清单, 花了钱不动账单——书里的 800 行模块之所以值,正因为它挂着 30% 的真金白银13
  • 性能剖析的前提取决于程序形态。交互式、IO 密集型的系统,「20% 代码耗 80% 周期」的形态 可能出现在等待上而非计算上——profiler 照样用,但优化对象变成 IO 而非算法(书的年代以计算为主)。

6. 可带走的

  1. 软件只要在用就会被改,一直改到重写更划算——维护不是阶段,是常态;
  2. 每次改动都在给结构留疤(熵增加);对抗它靠两条:管住每次改动的质量,管住每批改动的量;
  3. 别治症状:看到「总是 2 倍」,查为什么翻倍,不要除以 2——两个互相抵消的错误是不可维护的定义;
  4. 读代码时发现的疑似 bug,提交变更请求,不顺手修;
  5. 「这次改动很简单」这句话是预警信号,不是结论——念出它的那一刻,把评审和回归全部拉满;
  6. 每次改动后跑回归:验证新功能生效,更验证老功能还活着;
  7. 翻新按维护账单排优先级:重写那个占 30% 修复费用的 800 行模块,比全面翻新划算;
  8. 发版之间保持改动量平稳;大步改版必然「性能差、可靠性差、超支」;
  9. 优化前先剖析:80% 的时间在 20% 的代码里,其余的优化是浪费。

7. 原文地图

主题原书章原文位置
持续变化定律第9章 演变原则text/21-ch09.txt:19(搜「持续变化定律」)
熵增加定律第9章 演变原则text/21-ch09.txt:25(搜「熵」)
没坏别修(提交变更请求)第9章 演变原则text/21-ch09.txt:41(搜「记录并提交」)
治症状(2 倍/除以 2)第9章 演变原则text/21-ch09.txt:51(搜「2倍」) · text/21-ch09.txt:51(搜「除以2」)
先变更需求(SRS)第9章 演变原则text/21-ch09.txt:59(搜「SRS」)
发布前错误→发布后错误(填坑)第9章 演变原则text/21-ch09.txt:67(搜「填坑」)
越老越难维护(比例)第9章 演变原则text/21-ch09.txt:75(搜「比例」)
语言影响可维护性第9章 演变原则text/21-ch09.txt:83(搜「APL」)
有时重新开始(维护者会用文档吗)第9章 演变原则text/21-ch09.txt:91(搜「维护者们」)
先翻新最差的(800 行/30%)第9章 演变原则text/21-ch09.txt:99(搜「800行」)
维护引入更多错误(20%~50%)第9章 演变原则text/21-ch09.txt:107(搜「20%到50%」)
回归测试(三类维护)第9章 演变原则text/21-ch09.txt:99(搜「改正性维护」) · text/21-ch09.txt:113(搜「回归测试」)
自我失效模型第9章 演变原则text/21-ch09.txt:127(搜「自我失效」)
机械结构化≠更好第9章 演变原则text/21-ch09.txt:137(搜「同样糟糕」)
剖析先行(80/20)第9章 演变原则text/21-ch09.txt:145(搜「20%的代码」) · text/21-ch09.txt:145(搜「性能分析工具」)
熟悉守恒(改动量稳定)第9章 演变原则text/21-ch09.txt:157(搜「熟悉守恒」) · text/21-ch09.txt:159(搜「相对稳定」)
存在促进演变第9章 演变原则text/21-ch09.txt:167(搜「改变了这个环境」)
2021:第 9 章全部依然有效作者序text/08-fm.txt:73(搜「演变原则」)

Footnotes

  1. 出处:「第9章 演变原则」第 7 段(text/21-ch09.txt:7,搜「演变」)。演变的三种目的(新功能/更省/修错)同段。

  2. 出处:「第9章 演变原则」第 19 段(text/21-ch09.txt:19,搜「持续变化定律」)、第 25 段(text/21-ch09.txt:25,搜「熵」)、第 157 段(text/21-ch09.txt:157,搜「熟悉守恒」)、第 167 段(text/21-ch09.txt:167,搜「改变了这个环境」)。四条定律分别标注原始论文。

  3. 出处:「第9章 演变原则」第 19 段(text/21-ch09.txt:19,搜「持续变化定律」)。「任何正在使用的大型软件系统都将经历不断的变化,因为系统的使用会使人想出新的功能」同段。 2 3

  4. 出处:「第9章 演变原则」第 25 段(text/21-ch09.txt:25,搜「熵」)。「越来越复杂,并且变得越来越杂乱无章」「所有有用的软件系统都将朝着较低的可靠性和可维护性迁移」同段。熵的词义解释来自通用知识。 2 3

  5. 出处:「第9章 演变原则」第 157 段(text/21-ch09.txt:157,搜「熟悉守恒」)。高于平均的变化量的后果、稳定效应、心理熟悉度衰减同段;结论「保持产品发布版本之间的改动量相对稳定」(搜「相对稳定」)同段。 2

  6. 出处:「第9章 演变原则」第 167 段(text/21-ch09.txt:167,搜「改变了这个环境」)。「无论你认为自己多么完美地实现了需求,都必须为部署之后必要的变更做好计划」同段。 2

  7. 出处:「第9章 演变原则」第 51 段(text/21-ch09.txt:51,搜「2倍」)。解法一的两条理由、「两个相互抵消的错误」「无法被维护」、解法二(搜「除以2」)与正解,同段。 2 3 4 5

  8. 出处:「第9章 演变原则」第 41 段(text/21-ch09.txt:41,搜「记录并提交」)。「很有可能你会引入而不是修复一个错误」同段。

  9. 出处:「第9章 演变原则」第 107 段(text/21-ch09.txt:107,搜「20%到50%」)。「维护期间对程序的修改……引入的错误远远超过最初的开发阶段」同段。 2 3

  10. 出处:「第9章 演变原则」第 127 段(text/21-ch09.txt:127,搜「自我失效」)。放松警惕的因果链与三道护栏(核准/核查/回归)同段。 2

  11. 出处:「第9章 演变原则」第 113 段(text/21-ch09.txt:113,搜「回归测试」)。三类维护(改正性/适应性/完善性,搜「改正性维护」)、「大多数人绕过回归测试,因为他们认为自己的变更是没有影响的」同段。

  12. 出处:「第9章 演变原则」第 75 段(text/21-ch09.txt:75,搜「比例」)。「每次更改都会使所有后续的更改更加困难,因为程序的结构必然会恶化」同段。

  13. 出处:「第9章 演变原则」第 99 段(text/21-ch09.txt:99,搜「800行」)。温伯格案例:重写 800 行模块(占全部改正性维护成本 30%)。 2 3

  14. 出处:「第9章 演变原则」第 91 段(text/21-ch09.txt:91,搜「维护者们」)。「重建、翻新和逆向工程……其实这很难做」同段。

  15. 出处:「第9章 演变原则」第 137 段(text/21-ch09.txt:137,搜「同样糟糕」)。

  16. 出处:「第9章 演变原则」第 145 段(text/21-ch09.txt:145,搜「20%的代码」)。Pareto 定律、热点、性能分析工具(搜「性能分析工具」)同段。

  17. 出处:「第9章 演变原则」第 83 段(text/21-ch09.txt:83,搜「APL」)。

  18. 出处:「第9章 演变原则」第 59 段(text/21-ch09.txt:59,搜「SRS」)。「只有在这样,才比较有可能让客户、市场营销人员和开发人员对变更内容达成一致」同段。

  19. 出处:「作者序」第 73 段(text/08-fm.txt:73,搜「演变原则」)。原话:「对于第9章中介绍的演变原则,全部依然有效」——全书各章复审中唯一无任何附加说明的一章。

  20. 出处:「第9章 演变原则」第 67 段(text/21-ch09.txt:67,搜「填坑」)。「发布之前错误就比较多的组件,发布之后也会被发现会有比较多的错误」「被经验数据所充分支持的」同段;与原则 114(第 08 章 3.6)互证。