第 11 章 · 未雨绸缪
两段题记正好互补:斯威夫特说"不变只是愿望,变化才是永恒",罗斯福说"选择一种方法,试试看;如果失败了,没关系,再试试别的方法。不管怎么样,重要的是先去尝试。"
这一章讲了什么
本章分四段,从"要不要抛弃第一版"一路推到"系统为什么会腐烂"。
试验性工厂和增大规模。 作者借化学工业打头:实验室里能实现的反应过程,不可能在工厂里一步做成,中间必须有"试验性工厂"(pilot plant)来提供放大经验——海水淡化要先在日产 10000 加仑的试验场测试,再上 2000000 加仑的系统。软件行业面对同样的问题,却没有吸取教训:一个接一个的项目都是先设计算法、直接应用到待发布的软件里,再按进度把第一次开发的产品交给顾客。
他的判断很直接:对大多数项目,第一个开发的系统并不合用——太慢、太大、难以使用,或者三者兼有;要解决,除了重新开始别无他法。而且这条经验有普遍性:“所有大型系统的经验都显示,这是必须完成的步骤。“原因之一是知识和新概念会不断出现,即使最优秀的项目经理也不可能一开始就无所不知。
所以管理上的问题不是"要不要做原型”,而是选择题的两面:为舍弃而计划,无论如何一定要这样做。把原型直接发布给用户看似省了时间,代价却很高——用户用起来极度痛苦,重新开发的人被分散精力,产品的声誉受损,而"即使最好的再设计也难以挽回名声”。
唯一不变的就是变化本身。 第一步是接受事实:变化与生俱来,不是不合时宜的异常情况。他引 Cosgrove 的观察:开发人员交付的是用户满意度,而不仅仅是有形产品;用户的实际需要和感觉会随程序的构建、测试与使用而变化。硬件产品也要求满足需求,但物体的客观存在把用户对变更的要求阶段化了;软件易于掌握又不可见,于是它的构建者面对的是永恒的需求变更。
作者在这里划了一条界线:他并不主张所有目标和需求的变更都应该、能够或必须整合进设计,因为项目开始时建立的基准会随着开发不断抬高,抬到最后可能什么都做不出来。但目标、设计策略、技术上的变化都不可避免,事先为它们做准备,总比假设它们不出现要好。抛弃原型这个概念本身,就是对"随着学习更改设计"这一事实的接受。
为变更设计系统。 这一节讲的是常规做法:细致的模块化、可扩展的函数、精确完整的模块间接口设计、完备的文档,此外还可以采用调用队列与表驱动的技术。他特别点出最重要的措施——使用高级语言和自文档技术以减少变更引入的错误,以及用编译时的操作整合标准声明。再加上一条纪律:变更的阶段化——每个产品都要有数字版本号,每个版本都要有自己的日程表与冻结日期,此后的变更归入下一个版本。
为变更计划组织架构。 Cosgrove 主张把一切计划、里程碑和日程都当作尝试性的,作者认为这走得太远:当前小组失败的主因是管理控制得太少,而不是太多。但 Cosgrove 有一个卓越的观察值得留下——人们不愿为设计写文档,原因不只是惰性或时间压力:
通过设计文档化,设计人员将自己暴露在每个人的批评之下,他必须能够为他书写的一切进行辩护。如果团队架构因此受到任何形式的威胁,则没有任何东西会被文档化,除非架构是完全受到保护的。
也就是说,文档之所以写不出来,往往是因为写它的人不安全。接着他谈怎么为变更组建团队:分配的工作必须多样、富有延展性,团队可以灵活调动;大型项目需要两三个顶级程序员充当"技术轻骑兵",在开工最密集时扑向问题。系统变化时管理结构也要跟着调整,于是老板必须重视能力培养,让管理人员和技术人才具备互换性。
障碍是社会性的,他点得很直白:管理人员常觉得高级人员太"有价值",舍不得让他们去做实际编程;而管理职位又拥有更高的威信。业界给出过两种对策:贝尔实验室干脆废除所有职位头衔,每个专业人士都是"技术人员中的一员";IBM 则保留管理与技术两条晋升线,级别在概念上对等。他还要求管理人员参加技术课程、高级技术人员接受管理培训,高层人员要做好亲自参与开发的技术与情感准备——工作量很大,但很值得。而外科手术队伍式的团队,其整体观念是对这些社会障碍的彻底冲击:高级人才编程时不会觉得自降身份,组织架构又最小化了成员间接口,于是当架构必须变化时,重新安排整支队伍相对容易。
前进两步,后退一步。 发布之后的变化叫"程序维护",但作者强调它与硬件维护根本不是一回事。硬件维护包含三件事:替换损坏器件、清洁润滑、修改设计缺陷,而且多数修的是实现层缺陷,对用户常常不可见。软件维护不含清洁润滑与器件更换,主要内容就是修复设计缺陷,而且通常还附带用户能察觉到的新增功能。
他给了一组让人清醒的数字:广泛使用的程序,其维护总成本通常是开发成本的 40% 或更多;而且这个成本受用户数量影响很大——用户越多,发现的错误越多。麻省理工学院核科学实验室的 Betty Campbell 描出了版本生命期里的一个循环:新版本中先重现上一版本已修复的 bug,新功能带来新 bug,解决之后平稳几个月,接着错误率重新攀升——因为用户达到了新的熟练水平,开始使用那些新功能,高强度使用把不易察觉的问题挖了出来。
维护的根本麻烦在于:缺陷修复总会以 20%–50% 的几率引入新的缺陷,所以整个过程是"前进两步,后退一步"。为什么不能修得更彻底?他给两条原因:看起来微小的错误往往其实是系统级问题,不容易看出来,局部修复的工作量清晰而小,更大范围的修复却常被忽视——除非软件结构简单或者文档写得非常详细;而维护人员常常不是原来的开发者,而是初级程序员或新手。后果是每条语句的维护需要的系统测试比正常编程更多,“理论上每次修复之后都要重新运行先前所有的测试用例”,现实中回归测试必须接近这个理想,于是成本极高。他的结论落到设计上:采用能消除或至少能指明副作用的程序设计方法,会在维护成本上有很大回报;参与设计实现的人越少、接口越少,产生的错误也越少。
前进一步,后退一步。 最后一节引 Lehman 和 Belady 对大型操作系统系列版本的研究:模块总数量随版本号线性增长,但受影响的模块数量随版本号指数增长。所有修改都倾向于破坏系统架构、增加混乱程度(熵);用于修复原有设计瑕疵的工作量越来越少,用于修复"早期维护活动本身造成的漏洞"的工作量越来越多。系统越来越无序,修复迟早会失去根基——尽管理论上一直可用,实际上已面目全非,无法成为下一步进展的基础。再加上机器、配置、用户需求都在变,“现实系统不可能永远可用”,因此基于原有系统的重新设计是完全必要的。
全章收在一个近乎悲观的结论上:软件开发是减少熵的过程,所以系统本身处于亚稳态;而软件维护是增加熵的过程,“即使是最熟练的软件维护工作,也只是放缓了系统退化到非稳态的进程”。
我的判断
- “为舍弃而计划"是全书最容易被误读成浪费的一条。作者的意思不是"随便写个草稿扔掉”,而是把第一次开发当作获取知识的必经步骤:如果你不承认第一版会被抛弃,你就不会有意识地安排抛弃,最后就是把该抛弃的东西交付给用户。
- 我认同他对两类成本的排序:把原型发给用户,省下的是时间,付出的是痛苦、精力和声誉。后来流行的"最小可用产品"与这条并不相同——MVP 是刻意定义范围的首次交付,而这里说的是"明知不合适的系统"。区别在于是否诚实。
- “交付的是用户满意度,而不仅仅是有形产品"这句话,是这一章的枢纽。它把"需求变化"从道德问题(用户又改需求了)变成了物理问题(满意度是随使用变化的量),于是"为变更设计"就不再是妥协,而是设计目标的一部分。
- Cosgrove 关于文档的那段观察,我认为是本章最有社会学洞察力的一处:文档缺失往往是安全感问题,不是纪律问题。它和第 9 章"每个人都以为自己是争取小红花的学生"是同一种诊断——组织一旦让承认不确定变得危险,信息就会停止流动。
- 我怀疑"变更的阶段化"在今天的适用边界。版本号与冻结日期在交付型软件上仍然成立,但在持续部署的服务里,冻结与版本号的意义已经很弱;不过"变更要有阶段"这个思路可以换成别的形式(特性开关、灰度、发布批次),内核仍然有效。
- 维护成本 40% 以上、修复引入新缺陷 20%–50%、受影响模块指数增长——这三条构成了本章的量化骨架。它们合起来解释了一个反直觉的现象:系统不是被一次大改动毁掉的,而是被一连串正确的修改拖垮的。作者用热力学语言讲这件事(熵),我认为这是全书最恰当的比喻之一。
和我手上的工作有什么关系
- 我的项目里,“第一个系统并不可用"这条已经是常态,只是我过去把它当成能力不足的证据,而不是过程的一部分。按这一章,正确的做法是明确划出试验范围:哪些代码注定要重写、哪些是要留下的骨架,事先说清楚,就不必在重写时产生内耗。
- “维护比开发更贵"这条对嵌入式尤其成立:出厂之后,固件升级的成本远高于写代码本身。这给了我一个具体的判据——如果一个改动会引入必须回归验证的副作用,那么它在设计阶段就该被避免,而不是留到维护期付 20%–50% 的额外成本。
- “参与设计实现的人越少、接口越少,产生的错误越少"这条,是我目前工作方式的直接辩护:单人加少量 Agent 的编队,接口数少,维护期的回归面也小。反过来说,如果我要引入并发改动,就得为指数增长的受影响模块留出测试预算。
- Cosgrove 关于文档与安全感的那段,我用得上:这个站点的会话记录之所以能写下去,是因为没有评审会借它追究我;如果哪天要给这些笔记加外部批评的环节,应先考虑清楚"承认不确定是否安全”,否则笔记会退化成为自己辩护的材料。
- “技术轻骑兵"这个角色我打算显式建立:给自己留两三个可以随时扑上去解决的通用能力(构建、检索、批量改写),在任务最密集时调用——这比每次重新造流程划算。实际上
bin/build.sh与ocr-pdf.py就是这类轻骑兵的第一批成员。
一句话记住
第一个系统注定要丢掉,所以要为舍弃而计划;发布之后,维护会以"前进两步、后退一步"的方式不断增加系统的熵,减少熵的唯一办法是重新设计。