跳转到主要内容

1 - 第 1 章 · 焦油坑

大型项目的失败很少由某个单一问题造成,而是问题互相纠缠;而"程序"和"能交付的产品"之间隔着大约 9 倍的工作量。

开篇是一句荷兰谚语:“前车之覆,后车之鉴。” 接着是全书最出名的一个画面——史前巨兽陷在焦油坑里,挣扎得越猛烈,焦油纠缠得越紧,没有哪种猛兽有足够的力气或技巧挣脱,最后都沉到坑底。作者说,过去几十年的大型系统开发就是这样一个焦油坑。

这一章讲了什么

作者先用比喻立论:大型项目的困难不是由某个单独的问题造成的,每个问题单看都能解决,但当它们相互纠缠、累积在一起时,团队的行动就越来越慢。局中人往往对麻烦的程度感到惊讶,也很难看清问题的本质。他的态度是——想解决问题,先了解问题,于是这一章去认识"系统开发这个职业"本身。

第一件事是把"程序"和"产品"分开。图 1-1 画了一条演进路径,两条边界各自加价:

形态要求成本
程序 Program作者能在自己平台的自己环境里跑起来基准
编程产品 Programming Product任何人都能运行、测试、修复、扩展:风格通用化、输入范围适用、详尽的测试用例库、完备文档至少 3 倍
编程系统构件 Programming System按精确定义的接口编制,符合内存、IO、机时等资源限制,并与其他构件以各种组合测试过至少 3 倍
编程系统产品 Programming Systems Product同时满足以上两者约 9 倍

右下角那个 9 倍的形态,“才是真正有用的产品,是大多数系统开发的目标”。

第二件事是职业的两面。作者列了五条乐趣:创造的纯粹快乐;做出的东西对他人有用;把相互啮合的部件组装起来、看它们按预期运转的魅力;因为工作非重复而持续学习;以及在极其易于驾驭的介质上工作——“程序员,就像诗人一样,几乎仅仅在单纯的思考中工作”。

接着是五条苦恼:追求完美(“咒语"里错一个字符、一个停顿,魔术就不出现);目标、资源和信息都由他人设定,个人的权威和他所承担的责任不相配;必须依赖别人的程序,而那些程序往往设计不合理、实现拙劣、发布不完整或文档极差;概念设计有趣,但找琐碎的 bug 是重复劳动,而且调试往往是线性收敛、甚至二次方复杂度的,导致"寻找最后一个错误比第一个错误将花费更多的时间”;最后是产品刚完成就已显陈旧。

章末收束:这就是编程,一个许多人痛苦挣扎的焦油坑,也是一种乐趣与苦恼共存的创造性活动。对许多人而言快乐远大于苦恼,而本书后面各章要做的,是为通过这个焦油坑"搭建一些桥梁"。

我的判断

  • 9 倍这个数字是这一章我最实用的一条收获,它把"写产品和写程序不是一回事"从模糊的直觉变成了可讨论的量级。但它是经验数据,书里没有给出测量方式,所以我的态度是:方向可信,倍数别当真理。它最大的价值不在精确,而在于提醒你那些"非功能"工作(测试用例库、文档、接口规范、组合测试)本身就是主体工作量,不是收尾的装饰。
  • “挣扎得越猛,陷得越深"这个比喻容易被读成宿命论,好在作者自己在章末就拆掉了它——后面各章是"搭桥”。所以焦油坑不是物理定律,而是对蛮力的警告:越是想靠加人和加班硬顶,越容易被缠住。
  • 两组各五条的乐趣与苦恼里,最过时的是最后那条苦恼。1975 年的替代方案还在"构思和安排"阶段,如今技术栈的迭代周期以月计,产品完成即过时的压力其实是被放大了而不是减弱了。
  • 我保留怀疑的是"权威与责任不相配"这一条的推论。作者把它归为编程固有的苦恼,但在别的工程领域同样普遍,所以它更像是组织问题,而不是软件特有的属性——把它当作职业宿命,容易放弃本可以争取的授权。
  • 最认同的一句是"如果我们想解决问题,就必须试图先去了解问题"。它看起来像废话,但对照第 2 章那个著名的加人推演,就知道作者是真的按这句话在做:先把问题算清楚,再谈解决方案。

和我手上的工作有什么关系

  • 9 倍成本这条可以直接对应到我自己的项目:能跑的 demo 和能交付的东西之间,差距往往不在代码量,而在测试用例、文档、接口约束和组合测试。看过这一章之后,我在排期时会把这些显式写出来,而不是默认它们"顺手就做完了"。
  • “权威与责任不相配"精准描述了我见过的一类摩擦:底层驱动工程师对稳定性负责,却决定不了硬件选型和上层调度策略,出问题时责任在他、权限却不在他手上。
  • 依赖他人程序这条在嵌入式里尤其真实:拿到一份没有文档、没有测试用例的 SDK 或第三方库,研究与修改它的时间常常超过自己实现。反过来推给自己的要求就很清楚——我交付的东西至少要让人不用问我就能用起来。

一句话记住

没有哪个单独的问题会让项目失败,但所有问题会一起把你按进焦油坑;而"程序"和"能交付的产品"之间,隔着大约 9 倍的工作量。

2 - 第 10 章 · 提纲挈领

在堆积如山的文件里,少数几份文档是关键枢纽:目标、技术说明、进度、预算、空间与组织图,围着它们项目管理才转得起来。

这一章的题记干脆就是一个"假设":在堆积如山的文件资料中,少数文档是关键枢纽,每一件项目管理的工作都围绕着它们运转。这些文档是项目经理最重要的个人工具。

这一章讲了什么

作者先替新任项目经理说了句实话。技术、周边组织、行业传统等因素共同决定了项目必须准备哪些文书工作,而对一个刚从技术人员中提拔上来的经理来说,这些文档"令人生厌",看起来毫无必要、令人分心,充满被吞没的威胁——“但是,在实际工作中,大多数情况都是这样的。”

转变发生在认识上:文档的某些部分本身就承载着管理工作。每份文档的准备工作,是集中考虑并使各种讨论意见明朗化的主要时刻;不这样做,项目往往处于无休止的混乱中。文档的跟踪维护是项目监督和预警的机制,而文档本身可以充当检查列表、状态控制和汇报的数据基础。

为了说清软件项目该准备什么,他先去别的行业找共同点。

计算机产品的文档包括:目标(待满足的目标与需要、迫切需要的资源、约束和优先级)、技术说明(计算机手册加性能规格说明,是计划新产品时第一个产生、最后完成的文档)、进度、预算、组织结构图、工作空间的分配,以及报价、预测、价格这三份互相牵制的文档。他对预算的评价值得单独记:它"不仅仅是约束",而是最有用的文档之一——预算的存在会迫使制定技术决策,否则技术决策很容易被忽略,更重要的是它促使并澄清了策略上的决定。

报价、预测、价格构成一个循环:市场预测需要先有产品性能说明和假设价格;预测值连同设计得出的组件数量决定生产成本,进而决定单元开发工作量与固定成本,最后决定价格。价格低于假设值就进入良性循环——预测更高、单元成本更低、价格可以继续降;高于预测值则进入必须拼命打破的灾难性循环。作者说这种压力常常是激励市场人员和工程师的最佳动力,但也坦承它会带来"可笑的和摇摆":他见过一个三年的开发周期里,机器指令计数器的设计每六个月变一次——要性能时用触发器实现,要降成本时改用内存实现。而他所见过最好的一位项目经理,作用是充当大型调速轮,用惯性压平来自市场与管理的波动。

大学科系的文档几乎是一份镜像:目标、课程描述、学位要求、研究报告(申请基金还要求计划)、课程表与课程安排、预算、教室分配、教师与研究生助手的分配。唯一不需要的是价格文档,那由校董事会完成。作者说这种相似不是偶然——任何管理任务的关注焦点都是时间、地点、人员、项目内容和资金。

于是软件项目的文档应当包括:内容上的目标(待完成的目标、迫切需要的资源、约束和优先级)与产品技术说明(以建议书开始,以用户手册和内部文档结束,其中速度和空间说明是关键部分),时间上的进度,资金上的预算,地点上的工作空间分配,以及人员上的组织图。他特别点出组织图与接口说明相互依存,并引了 Conway 的规律:“设计系统的组织架构受到产品的约束限制,生产出的系统是这些组织机构沟通结构的映射。“Conway 进一步指出,最初反映系统设计的组织图肯定不会是正确的;如果系统设计可以自由变化,那项目组织架构就必须为变化做好准备。

最后回答"为什么要有正式的文档”,三条理由:

  1. 书面记录决策是必要的——只有记录下来,分歧才会明朗,矛盾才会突出。书写本身要经历上百次细小决定,正是这些决定让人从令人迷惑的现象中得到清晰、确定的策略。
  2. 文档是沟通渠道——项目经理会不断发现,许多理应被普遍认同的策略,完全不为团队中某些成员所知。而项目经理的基本职责是让每个人朝同一个方向前进,所以他的主要工作是沟通,而不是做出决定;文档能极大减轻他的负担。
  3. 文档是数据基础和检查列表——通过周期性回顾,可以清楚项目所处的状态,以及哪些地方需要重点更改和调整。

这里他顺手否掉了一类流行说法:销售人员吹捧的"完全信息管理系统”(管理人员输入查询,屏幕就显示结果)。他的理由很实在——管理人员只有一小部分时间(可能 20%)用来从自己头脑外部获取信息,其余时间都在沟通:倾听、报告、讲授、规劝、讨论和鼓励。不过对于基于数据的那部分,少数关键文档至关重要,足以满足绝大多数需要。

全章收在一段总结上:项目经理的任务是制订计划并实现计划,而只有书面计划才是精确和可以沟通的;计划包含时间、地点、人员、项目内容和资金;这几份少量文档封装了他大量的工作。如果一开始就认识到它们的普遍性与重要性,文档就会成为顺手的工具,而不是令人厌烦的负担——“通过遵循文档开展工作,项目经理能更清晰和快速地设定自己的方向。”

我的判断

  • 这一章看似在讲文书工作,实际回答的是第 7 章留下的问题:工作手册里该放什么。作者给出的答案是六个维度——内容、时间、资金、地点、人员,加上作为约束的优先级;这比"把所有文档放进一个目录"有用得多,因为它规定的是文档的种类而不是数量。
  • “预算是最有用的文档之一,因为它迫使技术决策"这一条,我认为是全章最反直觉也最实用的判断。约束不来自技术论证,而来自资源的边界;而边界会强迫你在设计期就拍板。这和第 5 章"给每个功能分配 m 字节、n 微秒"是同一个机制,只是尺度不同。
  • 那个"指令计数器每六个月换一次实现"的例子,我读的时候笑了一下——它不是能力问题,而是缺少一位充当调速轮的人。作者对那位"最好的项目经理"的描述是一个很有价值的角色定义:他的贡献不是做更多决策,而是减少组织的来回震荡。
  • “项目经理的主要工作是沟通,而不是做出决定"这句话,我在第 6 章见过它的另一种说法(没有人是顾问,每个人都要承担义务)。两处合起来看,作者对管理角色的定义是一致的:让信息与方向一致,比亲自拍板更重要。
  • 我对他否掉"完全信息管理系统"的理由持部分保留。他说管理人员只有 20% 时间从头脑外部获取信息——这个数字是 1975 年的经验值,今天的形态已经大不相同。但他指向的核心仍成立:信息系统的价值不在于信息总量,而在于它是否服务于正在做的决策。
  • Conway 定律被放在这里而不是单独的章节,我觉得位置很准:组织图和接口说明是同一件事的两个投影。这条规律在今天最有用的推论是——想改变系统结构,先改变沟通结构,反过来也一样,组织不动而只改架构,多半失败。

和我手上的工作有什么关系

  • 按这六类文档对照我手上的站点:目标(PLAN.md 与 docs/sessions/ 里的决定)、技术说明(docs/reading-protocol.md 与 AGENTS.md 的约定)、进度(几乎没有)、预算(完全没有)、空间(data/home/*.yaml 与栏目结构)、人员(我 + Agent,没有成文)。缺的正是进度与预算——这两样恰好是我最常凭感觉判断的部分。
  • “预算迫使技术决策"这条我打算直接落地:给站点设一个明确的内容预算(栏目数上限、每篇笔记的字数区间、首页卡片的数量上限)。有了这个边界,“要不要再加一个栏目"就不再是品味之争,而是预算问题。
  • “组织图与接口说明相互依存"给我的提醒是:当我给 Agent 划分职责(谁写内容、谁做构建、谁做校验)时,职责划分本身会决定产物的结构。作者说最初的组织图肯定不对,所以我应该把职责调整当成常态,而不是一次设计到位。
  • 我特别认同"书写需要上百次细小决定"这句。写会话记录的过程本身就逼我发现了几个之前含混的判断(比如第 5 章那句中译歧义),写作是思考的检查器,不是思考的转录——这也正是这个读书笔记系列值得用四节固定结构写下去的理由。

一句话记住

别把文档当负担:在堆积如山的材料里,真正起作用的是少数几份关键文档——它们覆盖内容、时间、资金、地点与人员,记录决策、充当沟通渠道,并让你随时知道项目处在什么状态。

3 - 第 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 就是这类轻骑兵的第一批成员。

一句话记住

第一个系统注定要丢掉,所以要为舍弃而计划;发布之后,维护会以"前进两步、后退一步"的方式不断增加系统的熵,减少熵的唯一办法是重新设计。

4 - 第 12 章 · 干将莫邪

工具应该由项目统一提供而不是各人自备:目标机器、仿真与编译器平台、受控的程序库、文本编辑系统,以及高级语言与交互式编程。

题记是一句谚语:“巧匠因为他的工具而出名。”

这一章讲了什么

作者先批评一种他称为"经营五金店"的做法:每个骨干人员都仔细保管自己工作生涯中搜集的一套工具集,把它们当作个人技能的直观证明——编辑器、排序、内存转储、磁盘空间实用程序各有一套。他说这对软件项目来说是愚蠢的,理由三条:项目的关键问题是沟通,个性化工具妨碍而非促进沟通;机器与工作语言一变,工具的生命周期就结束;而开发和维护公共通用工具的效率无疑更高。

但仅有通用工具不够——专业需要和个人偏好同样需要专用工具。所以他在第 3 章的外科手术队伍里为每个团队配了工具管理人员:管理通用工具、指导客户和老板怎么用,同时编制老板需要的专用工具。他还特别提醒一个反直觉的组织结论:把所有分散的工具管理人员集中成一个公共工具小组,效率反而不会更高——专用工具必须贴着具体团队的需求长出来。

需要项目经理计划组织的工具是一张清单:计算机设施(含硬件与使用安排的策略)、操作系统、语言及明确的使用方针、实用程序、调试辅助程序、测试用例生成工具,以及处理文档的字处理系统。接下来按机器角色分述。

目标机器是软件所服务的对象,程序必须在这一类机器上完成最后测试;辅助机器则在开发过程中提供服务。为原有机型开发新操作系统时,同一台机器会兼具两种角色。他对目标机器提的要求很具体:速度不必快,但要有若干兆字节主存、百兆字节在线硬盘和终端;字符终端够用,但必须比每秒 15 字符的打字机快;大容量内存能支持功能测试后的覆盖与裁剪,极大提高生产率。此外必须有调试机器或软件,让各类程序参数被自动计数与测量——他把内存使用模式称为"非常强大的诊断措施",能查出不合逻辑的行为或性能意外下降的原因。

机器时间的进度安排是这一节最生动的部分。OS/360 开发早期,机器时间极度稀缺:他们按经验提前预订了 System/360 的使用小时数,结果起初机器日复一日空着,突然有一天十六个系统全部上线,资源配给立刻失控——因为"每个人在同一时间开始调试自己的第一个组件"。他们先尝试集中机器与磁带库、组建专业操作团队、把调试任务批处理化,每天跑四次、周转 2.5 小时(实际要求 4 小时),甚至动用一台带终端的 1401 做调度与跟踪。但队伍仍然过度运转,经历几个月"缓慢周转、相互指责和极度痛苦"之后,他们改成把机器时间切成连续的块:例如整个十五人的排序小组拿到四到六小时的使用块,自行决定怎么用,即使机器空着别人也不能占用。

结果是:机器利用率可能略降,但生产率提高了。理由是"持续的精力集中能减少思考时间"——六小时里连续十次操作,比分散在间隔三小时里的十次操作产出高得多;每次冲刺之后,小组通常还要一两天做文档工作;而三人左右的小组最能有效共享时间块。作者还顺带记下一个跨越三代机器的现象:系统调试总是夜班性质的工作,“技术完全改变了,操作系统出现了,大家喜好的工作方式并没有改变”——因为它生产率最高。

辅助机器和数据服务一节列了六类基础设施:

  • 仿真装置:目标机器是新产品时,逻辑仿真装置能在真机出现之前提供调试平台,而且真机出现之后依然有用。作者强调一句关键话:可靠并不等于精确。仿真在某些细节上肯定无法与真机完全一致,但它的实现至少在一段时间里是稳定的,而新硬件不会。硬件故障往往是间歇性的,不确定性最糟糕,因为它剥夺了开发人员查找 bug 的动力——也许 bug 根本就不存在。
  • 编译器和汇编平台:编译软件应运行在可靠的辅助平台上,为目标机器产出目标代码,再立刻转到仿真装置上调试;用高级语言时,大量调试工作可以在真机全面测试之前完成。
  • 程序库和管理:OS/360 中做得最成功的一项,由 W. R. Crowley 领导,连接两台 7010 与一个大磁盘数据库。库按访问规则分成几层:开发库(playpen)里每位开发人员自由处置自己的程序,因为他是拥有者;准备集成时把拷贝交给集成经理放进系统集成子库,此后原作者未经批准不得再改;某个版本被广泛使用后提升为当前版本子库,原则上不可更改,用于所有新模块的集成与测试。程序目录跟踪每个模块每个版本的状态、用途与变更。作者从中提炼出两个理念——受控(拷贝由经理负责,变更需他独立授权)与让发布的进展正式化,并把开发库与集成、发布正式分离——并称这是"OS/360 工作中最优秀的成果之一",因为它其实是管理技术,贝尔实验室、ICL、剑桥大学都独立发展出了类似做法,而且同样适用于文档。
  • 编程工具与实用程序:内存转储、源文件编辑、快照跟踪、磁带拷贝、打印与目录管理等等——只要一开始就任命工具操作与维护人员,这些工作可以一次做好并随时待命。
  • 文档系统:最能节省劳动力的工具,是运行在可靠辅助平台上的计算机化文本编辑系统(他点名 J. W. Franklin 设计的那套)。没有它,OS/360 手册的进度会远远落后,而且更晦涩难懂。面对"六英尺厚的手册没用"的批评,他的回应是两条:文档规模虽大,但阅读计划是被仔细安排的,应把它当图书馆或百科全书,而不是一系列强制阅读的文章;同时他承认手册有不少地方需要改进,篇幅本可以大幅压缩。
  • 性能仿真装置:最好有一个,并且与逻辑仿真、产品本身使用同一套自上而下的设计方法;要尽可能早开始,并"仔细地听取它们表达的意见"。

最后一节讲当时最重要却未被广泛采用的两种工具:高级语言与交互式编程。作者的语气很硬——“我确信只有懒散和惰性会妨碍它们的广泛应用,技术上的困难不再成为借口”。

高级语言的价值在于生产率与调试速度:bug 更少且更容易查,因为它避免了在错误面前暴露所有级别的工作(不只是语法错误,还有寄存器使用不当这类语义问题),而编译器的诊断机制能帮忙定位。至于当年流行的三条反对意见——做不了我想做的事、目标代码太大、跑得太慢——他逐条回应:功能上的限制已经消失,只需要花时间找出做法;新的优化编译器在空间上已令人满意并会继续改进;速度上,优化编译器生成的代码比绝大多数程序员手写的效率更高,真遇到瓶颈,把其中 1%–5% 换成手写代码即可。至于选哪种语言,他认为当时唯一合理的选择是 PL/I。

交互式编程这一节里,他给了一份珍贵的量化证据(来自贝尔实验室 John Harr 的论文):

程序规模(指令)方式生产率(指令/人年)
ESS 代码800000批处理500–1000
7094 ESS 支持120000批处理2100–3400
360 ESS 支持32000交互式8000
360 ESS 支持8300批处理4000

结论是:系统软件开发中,交互式编程的生产率至少是原来的两倍。理由他也讲清了——调试是系统编程中最慢、最困难的部分,而"漫长的调试周转时间是调试的祸根"。最后他补了一条耦合关系:由于电传打字机和打印机终端无法胜任内存转储式调试,交互式工具的有效使用需要高级语言配合——有了高级语言,修改代码和选择性打印结果才变得容易。这两样东西是一对。

我的判断

  • 这一章表面在列工具清单,核心其实是一条组织主张:工具是项目的公共资产,不是个人的技能证明。作者给的三条理由里,第一条最容易被忽略——个性化工具妨碍沟通。工具不统一,本质上就是每个人的输出格式不同,交流成本随即上升。
  • 他对工具管理人员"不能集中成公共小组"的判断,与今天很多组织的直觉相反。我认同他的逻辑:专用工具贴着具体团队的需求,脱离上下文的集中供给会失效。这和站点里"约定要写在项目内的 AGENTS.md 而不是某个中心文档库"是同一件事。
  • “可靠并不等于精确"这句是我这一章最想带走的一句。它把仿真与真实环境的取舍讲透了:在开发阶段,稳定性比保真度更重要,因为不确定性会直接杀死排查 bug 的动机。这条今天仍然适用于容器、模拟器、桩件测试。
  • 程序库那三层(开发库 → 集成子库 → 当前版本子库)以及"受控"的理念,我认为是本章最实用的部分,而且它没有过时:今天的分支策略、代码所有权、发布冻结,本质都是同一套东西。它比"用什么版本控制工具"重要得多。
  • 我对"优化编译器生成的代码比绝大多数程序员手写更高效"这条持保留:它在 1975 年的系统编程语境下成立,但今天手写汇编在热点路径上仍能显著胜出(尤其是嵌入式)。不过我承认他的补充很务实——先全用高级语言,再把 1%~5% 换成手写,这个顺序是对的。
  • 交互式编程那组数据(2 倍)放到今天看反而更极端:现代开发几乎全在交互式环境里,“批处理"这个名字已经消失。作者在五十年前就把它列为"最重要却未被广泛采用"的工具,这个判断后来被完整验证了。

和我手上的工作有什么关系

  • 我的"工具集"目前散在两处:项目内的 tools/(Hugo、Go)与项目外的系统包(pdftoppm、OCR 引擎)。按这一章的标准,这已经算公共工具而非个人工具,但没有统一的说明。我打算在 AGENTS.md 里补一张工具清单,写清每件工具的用途与安装方式——这正是作者说的"工具管理人员"在单人项目里的最小形态。
  • 时间块那条对我有直接价值。我经常把多件事切碎并行处理,结果每件都处在"间隔三小时"的状态。作者的实测结论是连续集中的六小时远胜分散的六小时,因为省下的思考时间属于切换成本。我准备把"给一件事分配连续的时间块"当作排期规则。
  • “受控"那三层库让我重新看待自己的工作流:我的 content/ 相当于开发库(自由改),main 分支相当于集成子库(推送即对外),而发布出去的版本则是当前版本子库(不可更改)。差距在于我缺少集成经理这个角色——目前是我自己既当作者又当集成者,所以作者说的"原作者未经批准不得改动"在我这里形同虚设。至少要给发布加上"发布后改动必须记入会话记录"的约束。
  • 高级语言那条结论,与我在嵌入式上的现实有些出入:那里 C 与汇编仍是主流,编译器优化水平参差。但它的方法论仍可借用——先把热点交给工具,再用实测数据决定哪 1%~5% 值得手写,而不是凭直觉一开始就手写优化。
  • “可靠不等于精确"这条我打算用在测试环境上:与其追求与真机完全一致的模拟平台,不如先保证它稳定可复现;一个稳定但不完美的平台,比一个精确但飘忽的平台更能提高排查效率。

一句话记住

工具是项目的公共资产而不是个人技能证明:统一提供通用工具、贴着团队需求保留专用工具,并优先保证调试平台的稳定——因为不确定性会直接杀掉排查问题的动力。

5 - 第 13 章 · 整体部分

怎样造出能运行的系统:自上而下的设计剔除 bug,构件单元按计划调试,系统集成一次只加一个构件。

题记用的是《亨利四世》里的一段对话:“我可以召唤地下的幽魂。““这我也会,什么人都会,可是当您召唤它们的时候,它们会应召而来吗?“作者把它接到现代神话上:有人说自己能编写控制航空运输、拦截导弹、管理银行账户的系统,回答是一样的——我也可以,任何人都可以,但当你真的写出来,它会应召而来吗?

这一章讲了什么

这一章分三部分:怎么在设计阶段减少 bug、怎么调试单个构件、怎么把构件组装成系统。

剔除 bug 的设计

作者先给出 bug 的主要来源:系统各个组成部分的开发者都会做出假设,而这些假设之间的不匹配,是大多数致命和难以察觉的 bug 的来源。第 4、5、6 章讲的概念完整性正是直接对付这件事——它在让产品易用的同时,也使开发更容易、bug 更不容易产生。所以"详尽、艰苦的体系结构设计"不是洁癖,而是为了这个目的。他引贝尔实验室 V. A. Vyssotsky 的话:“关键的工作是产品定义。许许多多的失败完全是因为那些产品未精确定义的地方而导致的。”

由此引出四个具体做法:

测试规格说明。 在编写任何代码之前,规格说明必须提交给外部测试小组,详细检查它的完整性与明确性。理由由 Vyssotsky 说出来:“他们不会告诉你他们不懂。相反,他们乐于自己摸索出解决问题和澄清疑惑的办法。"——也就是说,规格说明里的含混之处,实现者会用猜的方式消化掉,而猜错的地方到很晚才会暴露。

自上而下的设计。 Niklaus Wirth 在 1971 年把许多优秀程序员多年使用的方法形式化:把开发划分为体系结构设计、设计实现、物理编码实现,每一步都用自上而下的方式。Wirth 把设计看成一连串精化步骤——先由粗略的任务定义和大概方案得到主要结果,再细查结果与期望的差距,同时把解决方案在更细的层次上分解;任务定义的每次精化都伴随算法精化,可能还有数据表达的调整。关键在于:一旦识别出模块,对它的进一步细化可以独立于其他工作进行,而模块的大小决定了程序的适用性和可变化的程度。他还主张每一步都尽量用级别更高的表达方法,直到非细化不可。

作者列出这样做的四条收益:清晰的结构更容易精确描述需求与模块功能;模块分割与独立性避免了系统级 bug;细节被抑制之后,结构缺陷更容易识别;设计在每一步都可测试,于是测试能尽早开始,且每步的测试落在合适的级别上。他补充说,按部就班不等于不能回头——至少它让人更清楚什么时候、为什么该抛弃整个设计重新开始,而糟糕的系统往往就是硬要挽救一个基础很差的设计、不停贴补丁。他甚至断言:十年内,自上而下的设计会成为最重要的新型形式化开发方法。

结构化编程。 这一系列方法主要来自 Dijkstra,理论证明由 Bohm 与 Jacopini 给出:程序的控制结构基本只保留语句形式的循环(DO WHILE)与 IF...THEN...ELSE 条件判断。作者也记下了反对意见——CASE 多路分支、异常跳转等结构很实用,完全禁止 GOTO 显得教条。但他指出真正的要点不在禁令:”把系统的结构作为控制结构来考虑,而不是独立的分支语句",这是程序设计发展史上向前迈出的一大步。

构件单元调试

作者说过去二十年调试技术走了一个大圈,四个阶段各有各的道理:

阶段做法与动机
本机调试早期机器 IO 差、延迟长,磁带读写让调试难以忍受,所以一次交互会话要尽可能塞进更多试验;程序员预先设计停止点、检查的内存位置与失败对策。花在写调试程序上的时间可能达到被调试程序编制时间的一半。“重大罪过"是没把程序切成测试段、没计划终止位置就按 START
内存转储本机调试效率高(两小时可能发现一打问题),但机时昂贵;有了在线高速打印机,改为"跑到检测失败就转储全部内存”,再回桌面逐个位置推敲。桌面时间没减少,但发生在执行之后、更含混,且受批处理周期拖累
快照内存从 2000–4000 字涨到无法整机转储,于是出现选择性转储、选择性跟踪和把快照插入程序的技术;OS/360 的 TESTRAN 允许插入快照而无需重新汇编编译,是这条路的终极产品
交互式调试1959 年 Codd 与 Strachey 分别发表分时调试论文,兼有本机调试的实时性与批处理的高利用率;配上高级语言与高效编辑工具,改代码和取快照都变得容易

交互式调试那一段藏着一个我觉得很有价值的实验:本机调试逼人预先计划,而交互式调试不需要——因为人思考时机器时间不浪费。但 Gold 的实验发现,每次调试会话中第一次交互取得的进展是后续交互的 3 倍。作者的解读是:缺少对调试会话的计划,我们并没有充分利用交互式调试的潜力,本机调试时代那段高效率的"计划期"消失了。

他因此给出自己的经验比例:正确使用良好的终端系统,往往要求每两小时的终端会话对应两小时的桌面工作——一半用于上次会话的清理(更新调试日志、归档更新后的程序清单、解释调试中出现的怪现象),一半用于准备(为下一次操作设计详细测试与改进计划)。没有计划,两小时的高产很难维持;没有清理,后续会话既不成体系、也无法持续推进。

系统集成调试

他把"出乎意料的困难部分"指定为系统集成测试,并重申两件事:系统调试花费的时间会比预料的更长,它的困难本身就证明了需要一套完备、系统化、可计划的方法。方法有六条:

  • 使用经过调试的构件单元。通行的看法是集成只能在各部分都能正常运行之后开始,但也存在两种不同做法:“合在一起尝试”(理由是除构件 bug 外还有接口等系统级 bug,早合拢早暴露)和"用各部分相互测试”(省掉搭建测试平台的工作)。作者说这两种想法都合理,但经验显示都不完全正确——在系统测试中使用完好的、经过调试的构件,比搭建测试平台加做全面单元测试更省时间。他还提到一种更巧妙的说法"文档化的 bug”:单元缺陷已被发现但未完全修复时就进入系统调试,测试人员知道这些缺陷的后果从而忽略它们。但他随即泼冷水:这多半是良好愿望,因为调试人员并不了解这些 bug 的全部后果,而记录在案的 bug 在被修复时又会注入新的未知问题。
  • 搭建充分的测试平台。测试平台指为调试服务、但不会进入最终产品的程序与数据,规模可能达到被测对象的一半代码量——他指出这是合乎情理的。三种形式:伪构件(只有接口和伪数据,例如排序程序未完成时先输出格式正确但无意义的有序数据)、微缩文件(只含典型记录但覆盖全部描述的小文件,专治对磁带磁盘格式的误解)、以及辅助程序(数据发生器、特殊打印输出、交叉引用分析等专用工具)。
  • 控制变更。硬件调试中严密控制变更的技术同样适用于软件:必须有人负责构件单元的变更与版本替换;必须存在受控拷贝——供单元测试的最终锁定版本、用于修缺陷的测试版本、以及其他人在其上开发的开发库。他讲了 System/360 工程里那个著名的做法:紫色线束——发现 bug 后先接一段紫色电线做快速修复,让测试继续,同时把更改记入日志、写正式变更文档、启动设计自动化流程,最终把调整落到电路图与线路中,紫色线束随之消失。软件同样需要这个手法:关键因素是对变更与差异的记载——日志里记录所有变更,在源码中显著标出"快速补丁"与"正式修改"的区别,而后者必须是完备、经过测试并被文档化的。
  • 一次添加一个构件。好处显而易见,但乐观与惰性总是诱人破坏它——因为每加一个构件都要配调试伪程序与测试平台,太麻烦了,“也许根本不会有 bug 吧?“作者的回应只有一句:拒绝诱惑。要假设系统里存在许多错误,并计划一个有序的过程把它们找出来。每加入一个新构件,都要用完整的测试用例跑一遍子系统——因为原来能在子系统上通过的用例,必须在新的整体上重新运行,也就是回归测试。
  • 阶段(量子)化、定期变更。开发者会不断拿来更快、更小、更完整或 bug 更少的版本,替换使用中的构件同样要走与新增构件一样的测试流程;此时测试用例通常更完整,测试时间反而会短很多。作者说这套阶段化变更非常优美地容纳了紫色线束技术:在下次定期发布之前一直使用快速补丁,而在当前发布中,只把已经过测试并文档化的修补整合进系统。

我的判断

  • 这一章是全书最"工程"的一章,也是最容易被读成清单的一章。但我认为它的主线只有一句:bug 的主要来源是假设不匹配,所以减少 bug 的功夫要花在定义与结构上,而不是花在更努力的调试上。后面所有清单都服务于这一点。
  • “外部测试小组审规格说明"这条我过去重视不够。Vyssotsky 那句"他们不会告诉你他们不懂"点破了一个普遍现象:实现者会用猜测填补规格的空白,而且不会主动报告自己猜了。这意味着未被发现的规格含混,最终都会以 bug 的形式出现,而且出现得很晚。
  • 自上而下设计那四条收益里,第二条"模块独立性避免系统级 bug"和第三条"细节抑制让结构缺陷更易识别”,我认为是它真正的价值所在,而不是"从上往下写"这个字面意思。至于作者"十年内会成为最重要的形式化方法"的断言,从今天的角度看并没有实现——结构化方法后来被敏捷实践挤到了背景里,但它作为局部工具仍然有效。
  • 我在上一章刚被"交互式调试效率翻倍"说服,这一章又看到 Gold 实验的反面:第一次交互的进展是后续的 3 倍。两条合起来才是完整的结论——工具给了你实时性,但如果用实时性替代计划,效率增益会被浪费掉。这解释了为什么我有时在终端里待了很久却收获很小。
  • “两小时终端会话配两小时桌面工作"这个比例,我认为是本章最实用的一句话,而且它反直觉:看起来在写代码之外的时间,其实占了另一半。其中"清理"那一半尤其容易被省掉,而作者说省掉它,后续会话就无法系统化和持续推进。
  • 紫色线束这个比喻太好了:允许快速补丁存在,但必须让它显性、可追溯、并且有明确的回收时机。它同时解决两个对立的问题——不因为细节不完美而阻塞测试,也不因为赶进度而让临时方案永远留在系统里。软件里的等价物就是"带标记的临时修改 + 修复期限”。
  • 我对"文档化的 bug"被否定这一点持部分保留。作者的理由(不了解后果、修复会注入新问题)在当时的调试条件下成立,但在今天的工程实践里,“已知缺陷 + 明确影响范围 + 后续修复计划"是相当常见且有效的做法。差别在于今天有自动化回归测试来兜住修复引入的新问题,而这正是他那个年代缺的东西——所以这条结论的适用性取决于测试基础设施的水平。

和我手上的工作有什么关系

  • 我的多数时间花在调试上而不是定义上,这一章正好说明问题出在哪:先写后想的代价会在后期以难以定位的 bug 形式偿还。对我这种一个人负责全部环节的项目,最划算的改变是把"规格说明"这一步显式化——哪怕只是写下接口、边界和失败行为,因为我既是实现者也是规格作者,天然最容易用猜测填补空白。
  • “外部测试小组审规格"在单人项目里的可行替代是:把规格交给一个独立的 Agent 去复述,看它复述出来的东西是否与我的意图一致。能复述对,才说明写清楚;这一步比我反复自读有效得多。
  • 紫色线束这条我马上能用:我经常在项目里留下"临时能跑"的取巧代码,然后忘了它。按这一章的做法,应该给临时补丁加显式标记(例如 # TEMP: 加日期与替代计划),并且规定它们必须在某个里程碑前回收。这样临时方案就不会悄悄变成遗产。
  • “一次添加一个构件 + 每次回归"这条对嵌入式尤其重要:硬件调试成本高,一次烧板改一堆东西会让排查无从下手。以前我靠自觉,现在有明确的规则:一次只加一个变量,并重跑先前的测试。
  • 测试平台的投入比例给了我一个心理许可:测试平台可能相当于被测对象一半的代码量,而且"合乎情理”。我以前总觉得写测试夹具是浪费,现在可以把它算进预算,而不是当成额外负担。

一句话记住

bug 主要来自各部分的假设不匹配,所以要靠定义和结构去剔除;到了集成阶段,唯一可靠的办法是假设错误大量存在、一次只加一个构件、每次都回归,并让临时补丁显性留痕。

6 - 第 14 章 · 祸起萧墙

项目是怎么晚了一整年的?一天一天晚下来的。灾祸多半来自白蚁而不是龙卷风,因此里程碑必须具体到无法自欺。

题记两句连读最有力量:索福克勒斯说"带来坏消息的人不受欢迎",紧接着自问自答——“项目怎么会被延迟了整整一年的时间?……延迟的时间是一天天积累下来的。”

这一章讲了什么

作者先破除一个想象:人们听说某个项目进度发生灾难性偏离时,会以为它遭遇了一系列重大灾难。但通常的情形是——灾祸来自白蚁的肆虐,而不是龙卷风的侵袭。重大灾害其实比较好处理,因为它们伴随着压力、重组和新技术的出现,项目组往往能应付自如;真正难对付的是那种以难以察觉、却残酷无情的方式慢慢落后。

他列了一串每天都在发生的小事:关键人员生病,会议开不成;雷击打坏了大厦的供电变压器,所有机器无法启动;工厂磁盘供货晚了一周,例行测试做不了。下雪、应急任务、私人问题、跟顾客的紧急会议、管理人员来检查——这个列表可以无限延长。每一件事都只拖延半天或一天,但整个进度就这样落后了。

里程碑还是沉重的负担

控制进度从制定进度表开始,表上的每一件事叫"里程碑",都带日期。日期的选择本质上是估计问题,很大程度依赖以往经验;而作者说里程碑的选择只有一个原则:必须是具体的、特定的、可度量的事件,能够清晰定义。

他给了几个反例,读起来很有既视感:编码在刚过一半时间时就已"90% 完成";调试大多数时候都"99% 完成";而"计划完毕"是任何人只要愿意就可以声明的事件。正面的例子则是百分之百的事件——“结构师和实现人员签字认可的规格说明”、“100% 源代码编制完成,纸带打孔完成并输入到磁盘库”、“测试版通过了所有的测试用例”。这类切实的里程碑,能澄清本来划分模糊的"计划、编码、调试"三个阶段。

他随后给出一个我觉得比"便于上级检查"更重要的判断:里程碑边界明显、没有歧义,比它容易被老板核实更重要。理由是:如果里程碑定义得足够明确、无法自欺欺人,很少有人会就进展弄虚作假;而里程碑一旦模糊,老板就常常收到与事实不符的报告——“毕竟,没有人愿意承受坏消息。这种做法只是为了起到缓和的作用,并没有任何蓄意的欺骗。”

他还引了政府承包商关于估计行为的两项研究,三条结论都反直觉:

  1. 若在活动开始之前就着手估计并每两周仔细修订一次,那么随着开始时间临近,无论最后情况多么糟糕,它都不会有太大变化;
  2. 活动期间,过高的估计会随着活动进行持续下降;
  3. 过低的估计在活动中不会有太大变化,一直到计划结束日期前大约三周。

最后他给里程碑定性:好的里程碑对团队其实是一项服务——一个可以向项目经理提出合理要求的依据;而模糊的里程碑是难以处理的负担。当里程碑没有正确反映损失的时间、并误导人们到事态无法挽回时,会彻底打击小组士气;慢性进度偏离同样是士气杀手。

“其他的部分反正会落后”

落后一天又怎么样?我们可以赶上来——何况和我们有关的其他部分已经落后了。作者承认这种心态背后有合理成分:进取心是优秀队员和团队不可缺少的心理素质,进取提供了缓冲和储备,让队伍能处理常规灾祸;而对任务进行计算、对工作量进行度量,确实会对"超前"造成一些消极影响。但他随即划出底线:仍然必须关心每一天的滞后,它们是大灾祸的基本组成元素。

既然不是每一天的滞后都等于灾难,那怎么判断哪些偏离是关键?他的答案是唯一的:只有采用 PERT 或者关键路径技术才能判断。它能显示谁需要什么、谁位于关键路径上、在哪里发生滞后会真正影响最终完成日期,也能指出一个任务在成为关键路径之前还有多少余量。书里的图 14-1 就是 OS/360 的一份报告摘录——按地点、任务、日期列出状态,正是这套机制的产物。

这一节最有价值的是他引贝尔实验室 V. Vyssotsky 的一段观察,它把"计划日期"和"估计日期"分开了:

计划日期是项目经理的工作产物,代表了经协调后的项目整体工作计划,它是合理计划之前的判断。估计日期是最基层经理的工作产物,基层经理对所讨论的工作有着深刻的了解,估计日期代表了在现有资源和已得到了作为先决条件的必要输入的情况下,基层经理对实际实现日期的最佳判断。

由此推出一条管理纪律:项目经理必须停止对这些日期的怀疑,把重点放在让它们更精确上——目标是得到没有偏见的估计,而不是合乎心意的乐观估计或者自我保护的保守估计。一旦这两类日期在每个人脑海中形成清晰印象,项目经理就能预见到:如果自己在哪里不采取措施,将来就会出现问题。

PERT 图的准备是老板与向他汇报的经理们共同的职责,但需要一个小班子(1–3 人)负责它的更新、修订与报告,可以看作老板的延伸。对大型项目,这种计划和控制小组的价值非常可贵:它的职权仅限于询问产品线经理什么时候设定或更改里程碑、是否达到了里程碑;所有文字工作由它处理,于是产品线经理的负担减到最少——只需要做决策。作者说自己运气不错,小组由 A. M. Pietrasanta 负责,此人在设计"有效而谦逊"的控制方法上很有天赋,结果这个小组被广为尊重,而不仅仅是被容忍——对一个本来就很敏感的角色,这是成功。

全章落在一句评价上:对计划和控制职能做适度的技术人力投资非常值得,它的贡献方式与直接开发软件差异很大——计划和控制小组作为监督者,明确指出了不易察觉的延迟、强调关键因素,是早期的预警系统,防止项目以一次一天的方式落后一年。

我的判断

  • 这一章解决的是第 2 章那个"进度灾难"的另一半:第 2 章解释为什么加人无用,这一章解释为什么问题总是在很晚才被发现。两章合起来才完整——灾难不是一次形成的,而是被一系列"不算灾难"的延误喂养出来的。
  • “灾祸来自白蚁而不是龙卷风"这个判断,我认为是全书最实用的一句组织学结论。它意味着风险管理的重点应该放在累积型的小事上,而不是戏剧性的大事件;后者有压力、有资源、有注意力,前者什么都没有。
  • “里程碑的清晰比可核实更重要"这一条我完全认同,而且它比表面看起来更深:模糊的里程碑不是"管理不便”,而是在制度上为自我欺骗留了空间。当一个人无法确定自己是否完成了什么,他倾向于选择对自己有利的解读;这不是道德问题,而是定义问题。
  • 那三条关于估计的研究结论里,最让我停住的是第三条:过低的估计在活动中不会改善,一直到结束前三周才变。它解释了为什么"进度落后"总是突然爆发——不是突然落后,而是突然承认。
  • Vyssotsky 对"计划日期"与"估计日期"的区分,是这一章的管理精髓。两者混在一起时,估计会被计划污染(“你应该能做到吧?"),从而失去预警价值。保护估计的诚实性,比让它准确更优先,因为没有诚实的估计,准确无从谈起。
  • 计划和控制小组的定位也值得学:职权被刻意限制在"询问和记录”,不介入决策。作者说这个小角色"被广为尊重而不仅仅是被容忍”——我认为关键正在于它的谦逊:它提供事实,不争夺判断。
  • 我对"必须用 PERT/关键路径才能判断"持一点保留。今天判断关键路径的工具已经很多(依赖图、燃尽图、关键链),形式不同,内核仍是"找出哪些延迟真的影响终点"。但作者那句话的要点是对的:不做路径分析,就无法区分"延误"与"无关紧要的波动"。

和我手上的工作有什么关系

  • 这一章几乎是在描述我的个人项目:单人开发没有"其他部分会落后"的借口,但仍然天天在消耗"半天到一天"的小延误,而且没有人替我记录。我以前把这些当成噪音,现在按作者的说法,它们是大灾祸的基本组成元素,应该被显式记下来。
  • “里程碑必须具体到无法自欺"这条我打算直接落地成写作与开发上的检查点,例如"这一章的笔记四节全部写完且构建零警告"这种可判定的表述,而不是"这章差不多写完了”。模糊的里程碑会让我对自己撒谎,这一点我在写这个系列的过程中已经体会到了。
  • 计划和控制小组在单人项目里的最小等价物是"记录 + 定期回顾":把延误与状态码写进 docs/sessions/,每完成一篇就回看一次偏差。它的价值不是问责(我无人可责),而是把不易察觉的延迟变得可见。
  • 区分"计划日期"与"估计日期"对我的用处很具体:当我给自己定下某个日期时,要分清这是"我希望的"还是"我基于现状的真实判断"。把前者当成后者,就是作者说的"合乎心意的乐观估计"。
  • 那三条估计研究结论给了我一个自查方法:如果我的任务总是"感觉快完成了",然后长时间没动,再突然延期——那就是典型的过低估计模式。结束前三周才开始变化是信号,不是意外。

一句话记住

项目不是被一场龙卷风毁掉的,而是被白蚁一天天啃空的:里程碑必须具体到无法自欺,每天的延迟都要记录,因为它们是所有大灾祸的基本组成元素。

7 - 第 15 章 · 另外一面

程序还有面向人的那一面:用户要九项说明,修改者要内部结构概述,而最省力的做法是把文档并进源程序本身。

题记两句放在一起,正好构成本章的两半:歌德说"不了解,就无法真正拥有",克雷布说"噢,赐予我朴素的评论者吧,他们不会因过于深奥而让人困惑不解"。

这一章讲了什么

作者先指出程序有"另外一面":它固然是从人传递给机器的信息,为了把意图传达给不会说话的机器,采用了严格的语法与严谨的定义;但书面的程序还有别的方式向用户讲述自己的故事。而且这件事即便对只给自己用的程序也必要——因为记忆会衰退,作者本人迟早会失去对程序的了解,不得不回头重拾当初劳动的细节。至于公共程序,用户与作者在时间与空间上都相隔遥远,文档的重要性更不必说:对软件产品而言,程序向用户呈现的内容与提供给机器识别的内容同样重要。

接下来他讲了一个故事。Thomas J. Watson 年轻时当收银机推销员,带着一马车机器满怀热情地出发,工作极其勤奋,却一台都没卖出去。他沮丧地向经理汇报,销售经理听了一会儿说:“帮我抬一些机器到马车上,收紧缰绳,出发!“在随后的客户拜访中,经理身体力行地演示了怎么卖收银机,这个方法奏效了。

作者说自己也讲过很多年课,热诚地向工程师们论述文档的必要性与优秀文档的特征——“不过,这些都行不通。我想他们知道如何正确地编写文档,却缺乏工作的热情。“后来他试着"往马车上搬收银机”,效果好得多。所以这一章不再说教,直接讲怎么做。

需要什么样的文档

他强调不同读者需要不同级别的东西,并分成三类。

使用程序的人需要一份描述文字。他批评多数文档"就像是描绘了树木,形容了树皮和树叶,但却没有一幅森林的图案”,然后给出九项内容:

  1. 目的——主要功能是什么,为什么开发这个程序;
  2. 环境——运行在什么机器、硬件配置与操作系统上;
  3. 范围——输入的有效范围,允许的合法输出范围;
  4. 实现功能和使用的算法——精确说明它做了什么;
  5. 输入—输出格式——必须确切且完整;
  6. 操作指令——含控制台与输出内容里正常、异常结束的行为;
  7. 选项——有哪些用户选项,怎样在其中挑选;
  8. 运行时间——指定配置下解决特定规模问题所需的时间;
  9. 精度和校验——期望结果的精确程度,以及如何检测精度。

他说这些内容三四页纸通常就能容纳,但要特别注意简洁与精确。而最关键的一句是:由于这份文档包含了与软件相关的基本决策,它的绝大部分必须在程序编制之前写出来。

验证程序需要测试用例。每份发布的程序拷贝都应带一些可例行运行的小用例,让用户确信自己拿到的是可信赖的拷贝、并且正确安装到了机器上。此外还需要更全面的用例供修改后常规运行,按输入数据范围分三类:测试主要功能的常规数据用例(占主体);数量较少的合法边界用例,覆盖最大、最小和其他有效特殊值;数量较少的非法数据用例,在边界外检查,确保无效输入能给出正确的诊断提示。

修改程序的人需要的是内部结构的概述:流程图或子系统结构图、所用算法的完整描述或等价算法的参考资料、对所有文件规划的解释、数据流处理的概要描述(从磁盘磁带取数到处理的序列,以及每一步完成的操作),以及初始设计中对已预见修改的讨论——特性与功能回调的位置、出口在哪、原作者对可能修改处的意见。他还补了一条我觉得很实在的:对隐藏缺陷的观察同样有价值。

流程图

这一节是全章的"反常识"部分。作者开口就说:流程图是被吹嘘得最过分的一种程序文档。很多程序甚至不需要流程图,很少有程序需要超过一页纸的流程图。理由是它只显示判断流向,而"仅仅是程序结构的一个方面”;画在一张图上时很优雅,一旦拆成几页、要靠编号出口和连接符拼装,整体概观就被严重破坏。

他还给出一个漂亮的推演:如果分组完成、每个方框只装一条语句,方框本身就成了重复练习、可以去掉;连接相邻语句的箭头也是冗余的、可以擦掉;剩下的只有 GOTO 跳转——而如果遵守块结构、消除 GOTO,连箭头都不见了。到那一步,流程图就可以丢掉,改用文字列表来表达同样的内容。

更值得注意的是他的经验判断:他从未见过有经验的程序员在动手写程序之前例行绘制详尽流程图;在被要求流程图的组织里,流程图总是事后补上的;有些公司则得意地用工具从代码反向生成这个"不可缺少的设计工具”。作者说这不是对良好实践的偏离,而是对技术的良好评判,它恰好告诉我们流程图真正有用在哪里。他引《使徒行传》里彼得的话收尾:“为什么让他们背负我们的祖先和我们自己都不能承担的重负呢?”

自文档化的程序

这是本章的落点,而且作者的推理方式很像他讲数据处理的思路:试图维持两份文件(程序与人读文档)之间的同步,是极其费力不讨好的事情;更合理的办法是让每条信息只存在于一处,于是应该把文档并入源程序。结果就是自文档化(Self-Documenting)程序。

他给出的三条基本方法:借助语言本身必须存在的语句来承载文档信息——标签、声明语句和符号名称都是工具;用空格与一致的格式表现从属与嵌套关系;以段落注释插入必要的记叙文字。他特别指出一个常见错配:很多程序在逐行注释上已经过量(尤其是为了满足公司呆板"良好文档"规范的程序),但段落注释仍然不够,而真正提供整体把握、加深理解的恰恰是段落注释。

由于文档是通过程序结构、命名与格式实现的,这些必须在第一次书写代码时就完成。作者接着列了十二条具体技巧,其中有几条今天已经是常识:用带版本号和可记忆名称的程序名;在过程注释里写记叙性描述;为基本算法提供参考文献;用助记符声明所有变量并用注释把声明补成完整说明;用标签标出初始化位置;对语句分组标记以对应算法描述的单元;用缩进表现结构;用行注释标记不清楚的地方;以及按逻辑思维把多条语句放在一行或把一条语句拆成几行。

他还逐条驳回了反对意见。最强的反对是源代码体积会变大——但他指出文本编辑正在向在线存储发展,程序与文字混在一起反而减少了需要存储的字符总数;至于"击键更多"的抱怨,答案类似:自文档化程序的字符总数更少,而电子草稿不需要重复打印。关于流程图和结构图,他的建议是折中:只保留最高级别的结构图,其余用文档;因为结构不常变,把它作为注释并入源程序更安全。

最后他给出适用范围:自文档化方法的基本思想可以大规模应用;对空间与格式要求更严格的那部分可能受限;而命名、结构化声明与段落注释在任何语言里都是好实践。全章收在一句立场上——高级语言的使用激发了这种方法,无论批处理还是交互式,它在在线系统上功效最强,因为是机器为人服务,而不是人为机器服务。

我的判断

  • 这一章与第 10 章构成互补:第 10 章讲项目管理需要哪些文档,这一章讲程序本身要携带哪些信息。而两章共享同一个洞察——文档不是附加物,它是交付物的一部分。
  • “不了解,就无法真正拥有"这句题记,我看完这章才理解它的分量。作者论证的对象甚至不是团队,而是未来的自己:记忆衰退会让作者失去对程序的了解。这句话为"给自己写文档"提供了比"方便别人"更硬的动机。
  • Watson 卖收银机的故事,是全书讲道理讲得最漂亮的一段。它的要点不是说教没有用,而是示范优于宣讲:让人看见做法,比让人理解必要性更有效。这一条我打算用在协作上——与其跟执行者强调规范,不如把规范变成可运行的脚本与模板。
  • 九项说明里,第 4 项"实现功能和使用的算法"与第 9 项"精度和校验"最常被省略,而它们恰恰是别人判断"这个程序能不能解决我的问题"的依据。三四页纸就能写完的东西,几乎所有项目都不写,这件事本身就说明文档缺的不是能力而是习惯。
  • “自文档化"这条我完全认同,而且它的现代形态比我预想的更彻底:Markdown 说明、代码内注释、类型声明、有意义的命名、仓库内的约定文件,本质上都是把文档压进同一处来源,避免两份文件漂移。这个站点每页自动产出 Markdown 孪生文件,也是同一种思路——同一份源,多种呈现。
  • 我对作者"流程图被过度吹捧"的判断持认同但有保留:在嵌入式与硬件交互的代码里,时序与状态流转用图表达确实比文字清楚。但他说错的地方不多,因为他的靶子不是"图形”,而是把流程图当成文档主体、并且在事后补画这种做法。
  • 十二条技巧里我最认同"段落注释比逐行注释更重要”。逐行注释解释的是"这行在做什么",而读代码的人往往已经能看懂这一行;段落注释解释的是"这里为什么要这样",那才是无法从代码本身复原的信息。

和我手上的工作有什么关系

  • 我给自己的项目补文档时,常常只写"怎么用",很少写"为什么这么做"。按这一章的标准,缺的正是第 4 项(算法与功能的确切说明)和第 9 项(精度与校验),而这两项恰恰是半年后的我最需要的信息。这篇读书笔记系列本身,其实就是在补这一类内容。
  • “把文档并入源程序"这条我打算落到具体做法上:继续用类型与命名承载信息,把难解释的取舍写成段落注释,并且在临时改动上加显式标记(这正好接上第 13 章的"紫色线束”)。凡是有两处需要同步的信息,迟早会不同步——这条铁律在这一章终于有了可操作的对策。
  • 我尤其认同"文档必须在写代码之前完成大部分"。这一章和第三章的结构师、第四章的体系结构是同一件事的不同侧面:规格先行。我的实际困难不是不愿意写,而是不知道写到什么程度算够——九项清单正好给了我一个可勾选的边界。
  • 测试用例那三类划分(常规、合法边界、非法输入)可以直接用在嵌入式与站点的构建上:常规用例保证主流程,边界用例覆盖最大最小与特殊值,非法用例验证错误提示是否正确。我此前只做第一类,另外两类的缺失正是"边界 bug"反复出现的原因。
  • Watson 的故事对我有一个具体提醒:如果想要协作对象遵循某种规范,把规范变成可执行的示例比写更多的说明有效——一个模板文件、一条构建命令,胜过一页约定。

一句话记住

程序还有面向人的一面:把用户要的九项说明、修改者要的结构概述都并进源程序本身,让信息只存在一处——文档不是附加物,它就是交付物的一部分。

8 - 第 16 章 · 没有银弹

软件的根本困难在于构造复杂概念结构本身;复杂度、一致性、可变性、不可见性决定了十年内不会有任何单项技术带来数量级的提升。

这一章是 1986 年的同名论文,收进纪念版后成了全书流传最广的部分。题记就是那句著名的判断:在未来的十年内,无论是在技术还是管理方法上,都看不出有任何突破性的进步,能够保证在十年内大幅度地提高软件的生产率、可靠性和简洁性。

这一章讲了什么

摘要与比喻

作者把软件活动分成两部分:根本任务是打造构成抽象软件实体的复杂概念结构;次要任务是用编程语言表达这些抽象实体,并在时空限制下把它们映射成机器语言。近年生产率的巨大进步都来自对次要障碍的突破——硬件限制、笨拙的语言、机器时间匮乏。于是问题变成一个算术题:除非次要任务占了全部工作的 9/10,否则即使把它压缩到零,也换不来数量级的提升。

所以他主张把注意力转向根本任务,并提出四条方向:做好市场调研、避免重复开发已上市的产品;把快速原型作为需求迭代的一部分;以"有机地生长"的方式增量开发;以及不断挑选和培养杰出的概念设计人员。

人狼的比喻是这一章的入口:民间传说里最可怕的妖怪是人狼,因为它会毫无预兆地从熟悉的面孔变成可怕的怪物;而软件项目也常常如此——看起来简单明了的东西,转眼就成了落后进度、超出预算、满是缺陷的怪物。为了对付人狼,我们在寻找银弹。作者说,找了十年,没找到。

但他特意补了一句:怀疑论者不是悲观主义者。虽然看不到单一突破,但已有的革新经过规范化、持续发展和传播,确实可能在将来带来数量级的提高——“虽然没有通天大道,但是路就在脚下。”

根本困难

这一节是全文的理论核心。他先纠正一个视角:软件看起来发展慢,其实是因为计算机硬件发展得太快——三十年里性价比提升六个数量级,人类文明史上没有第二个产业做到过。

然后借用亚里士多德的划分:把困难分成根本的(essence,软件特性中固有的)和次要的(accident,当前生产方式造成的、并非与生俱来)。根本困难在于软件实体必然是一个相互牵制关联的概念结构——数据集合、数据之间的关系、算法和功能调用。这些要素是抽象的,但内容极其丰富、要求高度精确。他的判断是:软件开发中真正困难的是规格说明、设计和测试这些概念结构的构造,而不是表达概念、验证实现的逼真程度。语法错误会有,但和概念错误相比微不足道。

由此他给出软件四个无法规避的内在特性:

特性含义
复杂度软件实体可能比任何人造物都复杂,因为没有哪两部分是相同的——如果相同,我们早就把它抽成子函数了;规模增加不是重复添加同类元素,而是加入不同元素,且它们之间的交互往往非线性递增
一致性物理学家相信自然界背后有简化原理,软件工程师没有这种安慰;他必须应付的很多复杂度来自人为惯例与系统接口,随心所欲、无规则、随时间变化,而且仅仅因为是不同的人设计的
可变性工业品出厂后很少改动,而软件持续承受变更压力:功能最容易感受变更压力,而且软件纯属思维产物、修改成本极低;成功软件的生命期又常长于它当初所依赖的硬件平台
不可见性软件没有空间形体,因此没有建筑平面图、机械制图或电路图那样的几何表达;用图形表达软件结构时,得到的是相互重叠的多张图(控制流、数据流、依赖、时序、名字空间),而且通常不是层次扁平的

他的结论是:软件的复杂度是根本属性,不是次要因素。数学与物理之所以能靠简化模型取得进展,是因为被忽略的复杂度不是研究对象的根本属性;当复杂度本身就是本质特性时,这条路走不通。这些特性还直接衍生出管理问题:复杂度让全面理解变得困难,妨碍概念完整性,让所有离散出口难以寻找和控制,最终把开发变成一场灾难。

三次真正的突破,都是次要困难

作者回顾了软件领域最有成效的三次进步,并逐一指出它们的边界:

  • 高级语言:最有力的突破,生产率至少提高 5 倍。它消除了整层不属于程序本身的复杂度(位、寄存器、通道这些机器语言关心的东西)。但它的作用是让表达更容易,并没有触及设计的复杂度;而且对不擅长使用复杂语言要素的人来说,它反而可能增加脑力负担。
  • 分时:价值在于及时性,让人能维持对复杂度的总体把握——批处理的漫长周转会让人遗忘细节,重新进入思考的代价极高。但作者指出,这同样是次要困难:它的效果随响应时间趋近人类可辨识的约 100 毫秒而趋于零。
  • 统一编程环境:Unix 与 Interlisp 通过集成库、统一文件格式、管道与过滤器,解决了"程序共同使用"这一次要困难;每个新工具都能通过标准格式被任意程序调用,于是又带动了整个工具库的发展。

逐一否决候选银弹

这一节是全文最容易被引用、也最容易招恨的部分。他对当时最被看好的技术逐条给出判断:

  • 面向对象编程:他自己就是看好者之一,并仔细区分了抽象数据类型与层次化类型这两个不同的进步来源。但他的结论是——它们消除的是设计表达上的次要困难,“除非不必要的低层次类型说明占据了软件产品设计的 90%,面向对象编程才能带来数量级上的提高”,因此对这颗银弹"深表怀疑"。
  • 人工智能:他借 Parnas 对两种 AI 定义的辨析,指出语音识别、图像识别与专家系统之间几乎没有共同技术,很难想象它们能给编程实践带来什么差异——“软件开发上的困难是决定说什么,而不是如何说”。
  • 专家系统:他给出全书对这类技术最公允的评价。它的真正贡献是把具体应用的复杂度与程序本身相分离,并把诊断规则以统一风格编码;而且产生诊断规则本身也是编制测试用例时必须完成的工作,如果做得好,可以减少测试设计的总工作量。但前提是"构建专家系统的必要前提条件是拥有专家",而知识获取的工作量是知识获取本身的两倍。他最终认为它最大的价值是传播优秀实践——让缺乏经验的人用上最优秀者的经验,因为软件工程中最好与一般实践之间的差距比其他工程领域都大。
  • “自动编程”:引 Parnas 的断言作结——“自动编程总是成为一种热情,使用现在并不可用的更高级语言编程的热情。“他指出多数所谓技术说明本质上是问题的解决方案,而不是问题本身。数据发生器、方程系统这类能做自动生成的应用有共同的简洁属性(参数少、已知解法多、选择规则清晰),因此难以推广到寻常系统。
  • 图形化编程:他认为至今没有令人信服的进展,而且"确信将来也不会出现”,理由是流程图本身就是很差的结构表达工具——“流程图已经被证明是完全不必要的设计工具,程序员是在开发之后,而不是之前绘制描述程序的流程图”。
  • 程序验证:它不能省略程序测试;更严肃的是,即使完美的程序验证,也只能证明程序满足技术说明,而这时软件工作中最困难的部分已经接近完成——验证实际上变成了对规格说明的测试。
  • 环境和工具、工作站:都会是有价值的改进,但按同样的算术,回报必然有限。他尤其点明:当机器速度再提高十倍,“程序开发人员的思考活动将成为日常工作的主要活动——实际上,这已经是现在的情况了”。

真正有希望的四条路

作者把生产率写成一条公式:任务时间 = ∑(频率)×(时间)。既然最耗时的是创造性工作,那么任何只加速"表达概念"的手段,都无法真正改变结果。因此他把希望寄托在能对付根本困难的做法上:

  1. 购买而非自行开发。他开玩笑说,构建软件最彻底的方案是不开发任何软件。软件成本一直是"开发的成本而非复制的成本”,所以共享能大幅降低每个用户的成本。他分析了为什么软件包在 1980 年代才被广泛接受:不是软件包变了,也不是需求变了,而是计算机硬件与软件的成本比率变了——花两百万买机器的人愿意再花二十五万做定制薪资系统,而花五万美元买办公机器的人不愿意。他还举了电子表格与简单数据库这些"戏剧性的例外":它们让大量不写代码的人用计算机解决了新问题。他的建议很具体:在生产一线配备个人计算机、装好通用软件、并培训会熟练使用它们的人。
  2. 需求精炼与快速原型。他认为开发中最困难的部分是确切决定要搭建什么系统,而"客户不知道自己需要什么",甚至不知道哪些问题必须回答。于是他下了一个明确的定论:在使用和开发客户定制系统之前,想要完整、精确、正确地抽取需求,“实际上也是不可能的”。因此最有希望的技术,是把快速原型化作为迭代需求过程的一部分——原型模拟关键界面、演示主要功能,不受真机速度与成本的约束,也不处理异常分支,目的是让客户能测试概念结构的一致性与可用性。他批评当时"先写清系统、再竞标、再开发、最后安装"的流程建立在根本错误的假设上。
  3. 增量开发——生长,而非搭建。他回忆 1958 年听到朋友说"搭建(building)系统"而非"编写(writing)系统"时的震动,随后又建议我们转向自然界:生物大脑的复杂程度远远超过任何对它的描述与仿真,其秘密是逐步发育成长,而不是一次性搭建。Harlan Mills 也曾建议所有软件系统都应以增量方式开发:先让系统能运行,哪怕只是正确调用一系列伪子系统;然后一点一点充实。作者说这种方法"在过去几十年中,没有任何方法和技术能如此彻底地改变我自己的实践"——它天然要求自上而下设计,让逆向跟踪变得方便,也天然利于原型开发。他还特别提到士气效应:当一个可运行系统出现时(哪怕极其简单),开发人员的热情会迸发出来;屏幕上映出第一个图案(哪怕只是个矩形),动力就成倍增长。他给出的经验是:团队可以在四个月内"培育"出比"搭建"复杂得多的系统。
  4. 卓越的设计人员。他把这一条放在最后,也放在最重的位置。作者承认方法论的价值——好的实践可以传授,所以颁布规范、编写教材、设立机构都是对的;但他断言良好设计与卓越设计之间的区别肯定不在于方法的完善性:“卓越设计来自卓越的设计人员。“软件开发是创造性过程,完备的方法学可以培养和释放创造性思维,却无法孕育或激发它。他因此提出最尖锐的一条批评:多数机构花大量资源寻找和培养管理人员,却没有任何一家在寻求和培育杰出设计人员上投入相同资源,而产品的技术特色最终依赖这些人。他的建议是:机构必须公开表明杰出设计者与卓越管理者同样重要,并在薪资、办公室、家具、设备、差旅与人员支持上完全对等;然后尽早、有系统地识别顶级设计人员(“最好的通常不是最有经验的”)、指派职业导师、制定并维护职业发展计划、提供相互交流与激励的机会。

我的判断

  • 这一章的真正贡献不是"没有银弹"这个结论,而是它给出的判断标准:任何新技术,只要它加速的是"表达概念"而非"构造概念”,那么它能带来的提升就有上限。这条标准让我能快速判断一个流行技术的天花板在哪——包括今天热得发烫的东西。
  • 那个 9/10 的算术值得单独记住。它把争论从"这个技术好不好"转成"它作用在哪一层”,而这正是大多数技术讨论缺失的维度。
  • 复杂性那四条里,我认为一致性最容易被低估。复杂度还能靠抽象与分层缓解,而"人为惯例、无规则、随时间变化、而且仅仅因为是不同人设计的"这一条几乎无法通过设计消除——它本质上源于软件必须与人和其它系统共存。
  • 我对作者几处"确信不会出现"的断言持保留:图形化编程在他身后的几十年里确实没有取代文本编程,但他对程序验证的处理略显轻率——今天的形式化方法与类型系统在很多关键系统里承担了实实在在的工作,即便它们确实不能省略测试。这类"我确信将来也不会出现"的句式,提醒我注意作者也有时代局限。
  • 专家系统那一节的公允程度出乎我的意料:他没有简单否定,而是承认"把具体应用的复杂度与程序本身相分离"是真实进步,并指出它最大价值在传播优秀实践。这个评价框架今天完全可以平移到对 AI 编码工具的判断上——它们的力量来自把经验编码并规模化传播,而不是来自解决了概念复杂度的根本问题。
  • “增量开发"那一段带着明显的个人情感,而且它构成了本系列最有意思的一处回响:作者说没有任何方法能像它一样彻底改变他自己的实践,理由之一是士气——有个能跑的东西,动力就成倍增长。这与我做这个站点的体验完全一致:先让站点跑起来再长大,比规划好一切再动手现实得多。
  • 最后那条"卓越设计来自卓越的设计人员”,是这一章最不技术、也最难接受的一段。它在一定程度上否定了方法论的作用,但我认为它是对的:方法能提高下限,决定上限的是人。它对个人项目的含义是——与其追求更多流程,不如把有限的时间投在判断力与品味的磨练上。

和我手上的工作有什么关系

  • 用 9/10 这个标准衡量我手上的工具:构建脚本、模板、OCR 流水线都在加速"表达与机械操作",它们的价值有上限;而我花在"想清楚要做什么"上的时间,才是真正决定产出的部分。不要指望工具解决设计问题——这条我在这个站点上已经验证过:省下的是打字时间,省不下的仍是决定写什么。
  • “购买而非自行开发"这条和我现在做的事在表面上相悖(我在从零搭站点),但逻辑一致:我购买的是 OINK 主题与 Hugo,而不是自己写文档框架。真正的自研只花在别人无法提供的那部分——我的内容与工作流。
  • 需求精炼那条对我有直接的减负作用:在动手之前不可能把需求想全,这不是我能力不足,而是这类工作的固有属性。所以正确做法是尽早做出可运行的东西(哪怕只是骨架),让"客户”(也就是未来的我)能对着实物说出真实需求。这个站点的系列骨架先上线、内容后填,正是同一路数。
  • 增量开发与士气那一段,解释了我为什么坚持"每完成一篇就构建、推送、上线":每一步都留下一个可运行的、可见的状态,比攒一大块再发更可持续。作者说团队能在四个月内培育出比搭建复杂得多的系统,我在这个系列上体验到的正是这个效应。
  • 设计人员那条给了我一个具体的行动项:把"个人项目的技术负责人"当成一个需要培养的角色来看待,而不只是一个身份。具体做法是保留决策记录(docs/sessions/ 与这个笔记系列),因为判断力的提升来自复盘自己的判断,而不是来自积累更多工具。

一句话记住

没有任何单项技术能让软件生产率提升一个数量级,因为根本困难在于构造复杂的概念结构本身;能真正改变结果的只有四件事——买而不是造、把需求做成迭代、让系统生长而不是一次搭成,以及培养卓越的设计者。

9 - 第 17 章 · 再论"没有银弹"

九年之后作者逐一回应批评:措辞的澄清、对"悲观"的辩护、面向对象这枚铜质子弹、重用的真实成本,以及为什么形势没有改变。

题记两句都在说同一件事:威廉三世说"生死有命,富贵在天",蒲柏说"任何人若想看到一件完美无瑕的作品,他所想的那种作品过去不存在,现在和将来也不会出现"。

这一章讲了什么

这一章是对"没有银弹"发表九年(1986 到 1995)后的回应与复盘。作者先交代了一个小插曲:那篇文章最初是 IFIP 1986 年都柏林大会的约稿,《计算机》杂志翻印时用了类似《伦敦人狼》的恐怖剧照当封面,还配了一篇《杀死人狼》的补充报道——“我并未注意到补充报道和插图,也没有料到一篇严肃的技术性文字会被这样润色”。

存在着银弹——就在这里

他重申自己的接受标准,而且这条标准很实在:当某个名副其实的中立客户走到面前说"我用了这种方法、工具或者产品,它让我的软件生产率提高了 10 倍",我很乐意接受银弹已经出现的观点。事实是,他重读 1986–1987 年间的早期反馈时发现,当时被强烈推崇的秘方并没有出现所声称的戏剧性效果。

含糊的表达将会导致误解

关于 “accident” 一词。他指出这个来自亚里士多德的术语被误读了:它不是"偶然发生",也不是"不幸的",而是"附带的"“从属的”。他也不曾贬低软件构建中的次要部分,反而认同 D. 塞耶斯对创造性活动的三阶段划分——概念结构的形式规格化、用现实介质实现、在实际使用中与用户交互;“必要"是构思概念结构,“次要"是实现过程。

关于争论的实质。他把问题还原成一个可测的现实问题:整个开发工作中,哪些部分与概念结构的精确有序表达相关,哪些是创造这些结构的思维活动?判断方式是看缺陷的性质——概念性的(比如没能识别某些异常)还是表达上的(比如指针错误、内存分配错误)。他的判断是:开发的次要或表达部分如今已经下降到整个工作的一半或以下。而且他强调这一点原则上可以用测量技术来研究;值得注意的是,从来没有人告诉他次要任务占了 9/10。

他还引了 Herzberg 等人 1959 年的研究(由 Bruce Blum 提示):动机因素能提高生产率,而环境和次要因素无论多么积极都无法提高生产率,只有在起负面作用时才会降低它。按这个框架,“没有银弹"说的正是——很多开发过程已经消除了那些负面因素:笨拙的机器语言、漫长的批处理周转、拙劣的工具、难以忍受的内存限制。

关于"因为是根本困难所以没希望"的误读。1990 年 Brad Cox 认为可交互的构件开发能解决根本困难,作者表示赞同,但指出 Cox 有两处误解:一是把他理解为"困难来自程序员缺乏技术”,而他坚持根本困难是固有的概念复杂性,任何时代、任何方法都存在;二是把他的结论读成"处理根本困难没有希望”——“这不是我的本意,每一种困难产生的麻烦都是可以改善的”。

由此他提出一个重要的限定:复杂性是层次化的。并非所有复杂性都不可避免,很多(不是全部)来自应用本身随意的复杂特性。他引 Sodahl 的观察:系统工作中遇到的大多数复杂性是组织结构失误的征兆,为这些现实建模、写出同等复杂的程序,实际上是隐藏而不是解决了混乱。Steve Lukasik 更进一步,认为连组织复杂性也不是任意的,可能受策略调整影响。作者以物理学家的背景回应:昨天的复杂性是今天的规律——分子的无序性启迪了气体动力学和热力学定律;他也承认自己没能力做更深层的分析,认为这门学科需要一种更广泛的信息理论,能像香农信息论量化交互流那样去量化静态结构的信息内容。

关于"悲观"的指责。他自嘲说,妻子、同事和编辑都发现他犯乐观主义错误的概率远大于悲观主义——毕竟是程序员出身,乐观是这个行业的职业病。而且文章里明确写过"怀疑论者并不是悲观主义者……虽然没有通天大道,但是路就在脚下”,还预言过:如果 1986 年的那些创新能持续开拓和发展,它们的共同作用就能带来数量级的提高。十年过后他的判断是:这个预言即使说明了什么,那也是过于乐观,而不是过于悲观。他用爱因斯坦的光速论断和哥德尔的不完备性做类比——一个结论让人沮丧,不等于它是错的。

他引 Turski 的回应作为这一节的收束,那段话用点金石的隐喻写得极好:炼金术里最诱人的目标是能把一般金属变成金子的点金石,它是从理想化想象和普遍假设中提取出的精华,是人类纯粹信仰的体现;即使被证明不存在,“寻找出路和希望能一劳永逸的愿望依然十分强烈”。于是——“将圆形变方的论文被发表,抑制脱发的洗液被研制和出售,提高软件生产率的方法被提出并成功地推销。“作者与 Turski 都认为,这个白日梦本身就限制了发展、浪费了精力。

随后他逐条回应 Harel 的批评。Harel 认为"消极"来自三个主题:把根本与次要划分开、独立评价每个候选银弹、只预言 10 年。作者的回应是:第一条正是文章的主旨,这个划分对理解软件为何难做绝对关键,也是改进方向的明确指南;第二条并不成立——各种技术是被逐个提出、且每一种都过分宣扬自身效果的,因此依次独立评估非常公平,他反对的不是技术,而是期望它们有魔术般作用的观点;第三条,选 10 年恰恰等于承认我们没能力预见 10 年以后的事——1975 年有谁能预见 1980 年代的微型计算机革命?而且所有银弹都宣称立刻见效,没有一种会说"投资我的秘方,10 年后你将成功”。

对 Harel 那个著名的思想实验——假设文章发表在 1952 年会怎样——作者也给了反驳:文章一开始就承认 1950 年代占支配地位的次要困难如今已不存在,消除它们已经带来数量级的改善;而且 Harel 对 1950 年代行业状态的设想并不准确。他列了一串事实:1952 年 Univac 用约八人开发复杂程序处理人口普查;汇编语言、重定位链接装载、浮点解释系统已被经常使用;1955 年已有 50–100 人年的商用程序;1956 年通用电气路易斯维尔工厂运行着超过八万条指令的薪资系统;1957 年 SAGE AN/FSQ-7 防空系统已经运行两年,是分布在 30 个地点、七万五千条语句的自消除故障热备实时系统。所以"个人程序的技术革命"并不能解释那段历史。

不过他也承认 Harel 有一部分感染了他:他自己喜欢问准程序员"下个 11 月在哪儿?",如果嫌问题太模糊,就再问"告诉我你自己关于时间历法的模型”——因为优秀程序员往往有很强的空间想象能力,心里装着时间的几何模型,而且高度个性化。

质量带来生产率,以及生产率数据的混乱

Capers Jones 提出一个视角转换:不要盯生产率,关注质量,生产率自然会跟着提高。理由是大量代价高昂的后续投入都花在寻找和修复规格说明、设计与实现上的错误,他给出的数据也显示了缺乏系统化质量控制与进度灾难之间的密切关系。作者认同这些数据,但补上 Boehm 的反例:一味追求完美质量,生产率会再次下降(他举 IBM 的航天飞机软件)。Coqui 的说法也被引用来佐证:系统化开发方法的发展本是为解决质量问题(尤其是避免大型灾难),而不是出于生产率考虑。

至于生产率数据本身,作者承认它非常难以定义、测量和寻找:Jones 相信相隔十年、功能等同的两个 COBOL 程序,用与不用结构化方法的差距是 3 倍;Yourdon 说"由于工作站和软件工具,我看到人们的工作获得了 5 倍的提高";DeMarco 则直说"我没有看到任何机构取得数量级的进步"。

这一节他最看重的是购买而非开发——而且他认为 1986 年那个"大众市场是软件工程领域意义最深远的开发方向"的估计被证实了。当软件包的销量达到百万甚至几千时,决定成败的问题就变成了质量、时机、产品性能和支持成本,而不再是开发成本。他给了一句很有画面感的建议:提高信息管理系统程序员生产率最戏剧化的方法,是去计算机商店买下本应由他们开发的成品;这些价格低廉、功能强大的软件更像电锯、电钻和砂磨机,而不是复杂的大型工具,因此必须"注重使用时的方便,而不是专业"。

这一节还有一段难得的自我修正。美国管理系统公司主席 Ivan Selin 在 1987 年写信质疑他"软件包没有真正改变很多"的观点,指出用户能感知到软件包变得更通用、更易定制,而这种感觉本身就会推动采用;他公司里多数情况下是最终用户而非软件人员不愿用软件包,因为他们担心失去必要特性。作者的回应很干脆:“Selin 是十分正确的——我低估了软件包客户化的程度和它的重要性。”

面向对象这枚铜质子弹

他先把问题放回"用更大的零件构建系统"这个框架:当零件很多、每个零件又很复杂时,只要接口流畅,丰富的结构就能快速组合起来。接着列出面向对象的几项特征——强制的模块化与清晰接口、封装、继承与层次化类结构、虚函数、强抽象数据类型化——并指出这些特征不必用整个 Smalltalk 或 C++ 就能获得,其中一些甚至出现在面向对象之前。他把它的吸引力比作复合维生素:一次再培训,拿到所有好处;结论是"一种非常有前途的概念"。

为什么发展缓慢?James Coggins 的解释是:面向对象的程序员经历的多是错综复杂的低层次应用,于是关注低层次抽象——链表、集合类的开发,而不是用户接口、射线束、有限元素模型;而 C++ 里帮助避免错误的强类型检查,反倒让"从小事物构建大物体"变得非常困难。他主张通过客户参与提高脑力劳动的规模,并赞同自上而下设计:“我的眼科客户并不关心堆栈,他们关心描述眼角膜形状的勒让德多项式。在这方面,小规模的封装带来的好处比较少。”

David Parnas 的回信则指向方法论:人们只被告知"面向对象是一种特殊工具",却没人教他们设计方法——“我们可以用任何工具写出优质或低劣的代码。除非我们给人们讲解如何设计,否则语言所起的作用非常小。结果人们使用这种语言做出不好的设计,没有从中获得多少价值。而一旦获得的价值太少,它就不会流行。”

作者自己给出的诊断是经济学式的:面向对象的前期投入很大,而收益严重滞后。它的优势是客观的,但要到后续的开发、扩展与维护中才体现出来——用 Coggins 的话说,“面向对象技术不会加快首次或第二次的开发,产品族中第五个项目的开发将会异乎寻常地迅速”。他由此判断,极度的前期投入与收益推后,是面向对象应用迟缓的最大原因;即便如此,在很多机构里 C++ 仍然取代了 C。

重用的真实成本

“解决软件构建根本困难的最佳方法是不进行任何开发”——软件包是途径之一,另一条是重用。作者引了一串互相印证的数据和意见:Jones 说多数有经验的程序员都有自己的私人库,约 30% 的代码是重用的;公司级重用能做到 70%,但需要专门的库和管理支持,还要求对变更做统计和度量。JPL 的 Van Snyder 指出数学软件领域长期重用的原因:障碍不在生产者一侧,而在消费者一侧——如果潜在使用者"觉得"寻找并验证已有构件比自己写更贵,重复的构件就会继续产生,而关键在这个"觉得",它和真实投入无关。数学软件之所以不同,是因为它难写(每行都要高智商投入)、又有丰富的标准术语来描述功能,于是重开发成本高、检索成本低;核反应、天气与海洋模型也有同样的条件。

而现实并不乐观:Parnas 说重用"说起来容易做起来难,它同时需要良好的设计和卓越的文档",没有好文档就看不到可重用的构件;Ken Brooks 说自己第五次使用个人界面库时仍在不断修改;DeMarco 干脆说"对于重用,现有理论几乎整体缺乏";Yourdon 的经验法则是可重用构件的工作量是一次性构件的两倍,作者则根据自己的产品化成本观察估算为三倍。

词汇量的代价

这是他给出的一个"可预见但还没被预言"的后果:思索的层次越高,需要掌握的基本要素就越多。要利用大型构件,无论对象类库还是程序库,都必须面对编程词汇规模的急剧扩大——现在已有成员超过三千的类库,很多对象需要十到二十个参数和选项,想拿到全部潜在重用,使用者必须学习每个成员的语法与语义。他认为这"并不是思维障碍中的一小部分"。

但也不绝望:一般人日常词汇超过一万个,受过教育者更多,而我们在自然语言中自然地学会了语法与微妙的语义——能区分"巨大、大、辽阔、大量、庞大",不会说"庞大的沙漠"或"辽阔的大象"。他由此提出三条教训:人们在上下文中学习,所以要出版复合产品的完整例子,而不只是零部件库;人们只背诵单词,语法和语义是在使用中逐渐习得的;人们按语义分类来组织词汇的使用规则,而不是比较对象子集。

结论

最后他回到基本问题:复杂性是这个行业的属性,也是主要的限制。他引 R. L. Glass 在 1988 年的总结——软件开发本来就是棘手的事情,前方不会有魔术般的解决方案,现在该做的是研究和分析革命性进展,而不是等待或希望它出现。他把这看作清新的空气:终于可以把焦点放在更可行的事情上,追求逐步的进展,而不是等待那个不大可能到来的突破。

我的判断

  • 这一章的价值在于它示范了如何回应批评:逐条区分"我表达不清"与"你理解有误",承认该承认的(如 Selin 指出他低估了软件包客户化),澄清误解的,并坚持核心论证。这种姿态在技术争论里很罕见——多数人要么全面防守,要么全面投降。
  • “accident 不是偶然或不幸,而是附带的、从属的"这个澄清很重要。中文译作"次要"大致抓住了意思,但仍容易读成"不重要”。作者的立场是:次要困难不该被贬低,它只是不该被误认为根本困难——否则改进的方向就会持续错位。
  • 他给出的可测化路径值得学:把"根本 vs 次要"从哲学争论变成分类问题——去看缺陷是概念性的还是表达性的。这比反复宣示立场有说服力得多。他也坦承没有人告诉他次要部分占 9/10,这正是把论证挂在可测事实上的好处。
  • “复杂性是层次化的"是这一章最容易被忽略的洞见。它意味着并非所有复杂度都命中注定,其中相当一部分是组织与策略失误的投影。Sodahl 那句"建立同等复杂的程序,实际上是隐藏而不是解决混乱"尤其锋利:我们常把组织问题翻译成技术复杂度,然后就再也拆不开了。
  • Turski 的点金石隐喻是我读过的对"银弹文化"最准确的讽刺。它的深刻之处不在嘲笑追求者,而在指出这种追求本身消耗了本该用于逐步改进的精力。
  • 关于面向对象,作者的诊断今天依然成立,只是对象换成了别的技术:前期投入大、收益滞后,是新技术推广迟缓的最大原因。Coggins 的"不会加快第一次和第二次开发,第五个项目才会"是一条极好的采纳判据——它要求我们诚实地问自己:我有没有"第五个项目”?
  • 重用那一节给出的三条判据我认为可以直接搬用:障碍在消费者一侧;重用的前提是良好设计与卓越文档;可重用构件的工作量是一次性构件的二到三倍。“觉得比自己写更贵就不重用"这句话尤其值得记住,因为它把问题从技术可复用性转到了检索与信任成本上。
  • 词汇量的代价是我此前没有认真想过的一条。类库越大,使用者需要记住的语法与语义就越多,重用的收益会被学习成本吃掉一部分。他提出的"上下文学习"结论对今天的意义比对 1975 年更大:示例与复合产品比 API 清单更能让人学会使用。

和我手上的工作有什么关系

  • 这一章的"可测化"思路我打算直接用在自我评估上:当我说"某件事很难"时,先分清它难在概念设计还是难在表达实现。这个划分能让我判断该换工具还是该停下来想清楚——换工具解决不了概念问题,这正是第 16 章那个 9/10 算术的现实版本。
  • “动机因素提高生产率,环境和次要因素只能拖累不能提升"这条框架,解释了工具投资回报的天花板:把环境从差改善到不差,收益很大;从不差继续改善到很好,收益就平了。所以我的 tools/、bin/build.sh 这类投入,价值主要在消除摩擦,而不是提高上限——这也让我不再期待它们带来神奇效果。
  • 面向对象的采纳判据给了我一把尺子:任何新技术值不值得投,关键看有没有"第五个项目"来摊薄前期成本。对单人项目而言,这意味着不要为不存在的规模预先付出架构成本;同时,重复出现的同类任务(比如这个读书笔记系列)恰恰是"第五个项目"的雏形,值得为它做一次性的方法投资。
  • 重用的三条判据我已经在实践中撞到了:这个站点把 OINK 主题复用过来,省下的是开发成本,付出的是学习主题约定的时间。Van Snyder 所说"障碍在消费者一侧"正是我的体验——寻找与验证既有方案的信任成本,常常高于自己写的心理成本。一旦承认这一点,学习成本就该被算进预算,而不是当成额外开销。
  • “示例比清单更能教会使用"这条我立刻能用:这个读书笔记系列本身就是一个复合产品的例子,而每篇里"和我手上的工作有什么关系"那一节,实际上是在为将来的自己提供上下文。作者说人们在上下文中学习,这个系列的设计正好符合这一点。
  • 最后那句"追求逐步进展,而不是等待不大可能到来的突破”,对我有直接的行为含义:与其寻找一次性把读书笔记写好的方法,不如接受一篇一篇地写、每篇都留下可运行的状态——这正是这个系列已经跑通的节奏。

一句话记住

九年过去,形势没有改变:复杂性是这个行业的属性;所有银弹都宣称立刻见效,而真正的进展来自承认根本困难、放弃一劳永逸的幻想,然后一点一点改善。

10 - 第 18 章 ·《人月神话》的观点:是与非

作者把 1975 年版的全部论断原样抽出来逐条自审:哪些被经验证实,哪些已经过时,以及他在哪里被说服改了主意。

题记是塞缪尔·勃特勒的一句俏皮话:“我们理解也好,不理解也好,描述都应该简短精练。“这一章正是照此办理——把全书论断压成一张清单,且一个字都不改。

这一章讲了什么

作者的动机写在开头:现在对软件工程的了解比 1975 年多得多,那么旧版里的观点,哪些得到了数据和经验的支持,哪些被证明不正确,哪些随着世界变化显得过时?为了便于判断,他把 1975 年版书籍中的论断毫无更改地抽取出来,以摘要形式列出,只标注"我认为是正确的:是客观事实和经验中推广的法则”;方括号里的评论是新版新增的内容。

所以这一章形式上很像附录,但读法不同:前面的章节是论证,这一章是判决。它的价值不在于新信息,而在于一位作者在二十年之后,回头给自己当年的断言逐条打分。

清单的覆盖范围与组织

命题按原书章节分组,从第 1 章《焦油坑》一直列到第 15 章《另外一面》——也就是 1975 年原版的全部内容。每一条都是陈述句,可以独立判断真假,例如"编程系统产品的开发工作量是供个人使用的独立构件程序的 9 倍”、“人月是危险和带有欺骗性的神话,因为它暗示人员数量和时间是可以相互替换的”、“Brooks 法则:为进度落后的项目增加人手,只会使进度更加落后”。

有几条被压缩得极好,几乎可以直接当格言用:“良好的烹饪需要时间,某些任务无法在不损害结果的情况下加快速度”;“任何创造性活动都伴随着枯燥艰苦的劳动,编程也不例外”;“人们通常期望项目在接近结束时收敛得快一些,然而情况却是越接近完成,收敛得越慢”。

最有意思的部分:他标注了什么

清单里最值得读的是那些带方括号的更新,以及少数几条被直接判为过时的命题:

原命题二十年后作者的批注
暂存区空间尺寸与每次磁盘访问的程序数量是关键决策已显得过时——起初因为虚拟内存,后来因为内存成本低廉;现在用户通常会买能装下主要应用全部代码的内存
库中每个组件需要两个版本(快的和短小的)现在看来,这有些过时了
软件工程师不会常规地对试验性系统做现场测试现在这已成为普遍实践,beta 版本不同于有限功能的原型,alpha 版本同样是他倡导的做法
系统编程可供合理选择的语言是 PL/I不再正确
只有懒散和惰性会妨碍高级语言与交互式编程的广泛应用如今它们已在全世界使用
流程图是被吹捧得最过分的文档补一句更狠的:流程图是图形化的高级语言;并说在"很少需要超过一页纸流程图"这一点上,MILSPEC 军用标准文档需求实在错得很厉害
OS/360 是画蛇添足(第二系统效应)的典型Windows NT 似乎是 20 世纪 90 年代的例子
采用时间块安排稀缺机时在 1995 年依然如此
Gold 实验:交互式调试中第一次交互的进展是后续的 3 倍我认为在 1995 年依然会如此

他也如实记录了自己的判断分歧:第 8 章的 1.5 次幂规律旁边注明"Boehm 的数据并不完全一致,在 1.05 和 1.2 之间变化";第 4 章"功能与复杂程度的比值"后面补充说这个比值是对易用性的一种测量,要由简单与复杂应用共同验证;“体系结构与实现分离"则被追认为同样适用于小型项目。

还有一条我认为是整章最诚实的自我修正。第 7 章工作手册一节里,1975 年的原文写道:

Parnas 强烈地认为使每个人看到每件事的目标是完全错误的;各个部分应该被封装,从而没有人需要或者被允许看到其他部分的内部结构,只需要了解接口。 Parnas 的建议的确是灾难的处方。(Parnas 让我认可了该观点,使我彻底地改变了想法。)

括号里的那句话,是二十年后加上的——他公开写下了自己当年判断错了。

此外他从别人的实践里补进了几条当代经验:微软的 Jim McCarthy 说"如果你错过了一个最终期限,确保完成下一条最终期限”;有个微软团队采用了非常小而频繁的阶段(量子)变更,结果每天晚上都需要重新编译生成增长中的系统。

我的判断

  • 这一章的方法本身值得学:把论断写成可判真假的陈述句,逐条留档,多年后回头打分。绝大多数作者(包括我自己写文档时)会把判断埋在流畅的叙述里,以至于几年后无法分辨哪句是当时的观察、哪句是一时的情绪。
  • 他标注过时的那些条目有一个共同规律:它们都依赖于当时的资源约束(内存贵、机时稀缺、语言选择有限、原型不被接受)。约束一变,结论就失效。这印证了第 5 章的提醒——评估任何建议之前,先问它依赖的假设是否还成立。反倒是"人月不可互换"“概念完整性优先"“缺陷修复会引入新缺陷"这类关于人与认知的命题,二十年过去一条都没过期。
  • “Parnas 让我认可了该观点,使我彻底地改变了想法"这句,是全书对我触动最大的一处。它不是客套,而是把一段具体的技术分歧记录成了判断的变更史。承认自己当年错了,比坚持一条正确结论更需要底气——尤其在书已经被引用二十年之后。
  • 我对"流程图是图形化的高级语言"这个说法持保留:作为对流程图崇拜的反讽很精彩,但它也解释了他为何整体看低可视化表达;在第 17 章里他仍然承认图形化编程"没有令人信服的进步”,而今天的数据流图、状态机图、时序图在工程沟通中的价值是有目共睹的。这是他的一个持续盲区,而不是精确判断。
  • 清单里有一类命题我认为被高估了:那些以经验数值形式出现、但样本很小的结论(比如"两个人团队常是最佳的人员使用方法”)。它们在清单里被列为事实,可实际上更接近当时的观察。作者本人在别处承认数据稀缺,这一章却把它们统一称做"客观事实和经验中推广的法则”,标准略宽。
  • 不过这一章的存在本身给出了一个反例,反驳了"方法学无用"的悲观解读:一份可被逐条检验的论断清单,正是方法学能提供的东西。它不保证结论正确,但它保证结论可被证伪。

和我手上的工作有什么关系

  • 我打算借用这个方法给自己的工作留一份"论断清单":在 docs/sessions/ 里把当时的判断写成可以判真假的句子,而不是只记"做了什么"。理由很实际——我现在回看几个月前的技术选择时,常常分不清当时是权衡后的结论,还是顺手为之。
  • 那些"依赖资源约束"的失效命题,正好是我评估旧技术笔记时最需要的判据。我的笔记里也有大量类似判断(“某某方案太占内存所以不用”),而内存价格与工具链已经变了几轮。以后写这类结论时要显式写下它依赖的假设,方便未来的自己判断是否已过期。
  • “Parnas 那一句"给了我一个具体的写作纪律:当我在笔记里不认同某个做法时,要写清是哪一类理由(知识不足、逻辑不通、还是约束不同)。这样即便后来改主意,也能知道是被什么说服的。这正是阅读协议里"评论要落到具体理由"那一条的实践价值。
  • 他补进来的当代经验提醒我:这一章的更新大多来自别人告诉他的事。这一节几乎全是引用 Jones、Boehm、McCarthy、Yourdon、DeMarco、Parnas 等人的来信与评论。对我这种独立工作的人来说,这是个缺口——我缺少这种"同行来信"式的纠错渠道,能做的替代是把结论公开写出来,接受读者的反驳。
  • 最后一条与这个系列本身有关:这份读书笔记写到现在,已经有十八篇结论。按这一章的思路,我应该在结束语那一篇里回头做同样的事——列出自己从这本书里得到的论断,并标明哪些是我确认的、哪些还在存疑。这比再写一段抒情式总结有用得多。

一句话记住

把论断写成可判真假的句子、逐条留档,二十年后回来打分——这既是对读者的诚实,也是唯一能让经验真正沉淀下来的方式;而经受住时间检验的,几乎都是关于人和认知的命题,不是关于当时资源约束的命题。

11 - 第 19 章 · 20 年后的《人月神话》

二十年后的自我清算:概念完整性与结构师依然成立,瀑布模型错了,Parnas 是对的,而微计算机革命与元编程改变了整个行业。

题记两句针锋相对:帕特里克·亨利说"只能根据过去判断将来",埃德蒙·伯克说"然而永远无法根据过去规划将来"。这一章正站在这两句之间。

这一章讲了什么

为什么要出版纪念版

作者用一个场景开场:飞机上,邻座的陌生人正在读《人月神话》,他一路等着看对方会不会有反应,最后在机舱门口忍不住问"这本书如何?“对方回答——“噢!这里面的东西我早就知道。“他决定不介绍自己。

这个段子后面接着一个真问题:一本 20 年前写的、讲 30 年前经验的书,为什么还相关?他反驳了两种常见解释。第一种说"软件开发学科没有正确发展”,靠软硬件生产率对比来支持;他指出反常的不是软件慢,而是硬件快得不符合人类历史的常理——那是制造业从装配转向流水线、从劳动密集转入资金密集的结果。第二种解释是这本书其实在讲"团队中的成员如何创建事物”,软件只是顺带提及。他认为这个说法有些道理:“人类历史是一个舞台,总是上演着相同的故事……剧本变化非常缓慢,而舞台的布局却在随时改变。“所以这本书是关于人与团队的,它的淘汰过程会是缓慢的。

依然成立:概念完整性与结构师

他重申:整洁优雅的编程产品必须给每位用户提供条理分明的概念模型——描述应用、实现方法以及用户界面的使用策略;而用户感受到的概念完整性是易用性中最重要的因素。他用 Macintosh 应用界面的统一作为正面例子,也承认 MS-DOS 那种粗糙但统一的接口同样有效。

接着重申第 3、4 章的角色安排,并且语气比 1975 年更确定:委派一名产品结构师是最重要的行动。结构师负责用户能实际感受到的概念完整性,是概念模型的所有者、用户的代理,在功能、性能、规模、成本与进度的权衡中代表用户的利益。这个角色是全职工作,只有在最小的团队里才能与经理合并——他给了一个精确的比喻:”结构师就像电影的导演,而经理类似于制片人。“对大型系统,还要由主结构师把系统分解为子系统,让边界落在接口最小、最易严格定义的地方,并递归下去。

他甚至说,在软件工程实验课上坚持让每四名学生的小组选出不同的经理与结构师——“在如此小的队伍中定义截然不同的角色可能有点极端,但我仍然发现这种方法即使对小型团队也运作良好”。

通用工具的新难题:盲目的功能与频率猜测

个人计算机革命带来一个新处境:结构师要为大型、不确定的用户群设计,而不是为某个公司的单一应用设计。他随即指出一个悖论:“设计通用工具比设计专用工具更加困难,这是因为必须为不同用户的各种需要分配权重。”

由此产生"盲目的功能”:以性能甚至易用性为代价,不断增加边界实用功能。功能建议在初期总是很吸引人,性能代价要到系统测试才显现,而"随着功能一点一点地增加,手册慢慢地变厚,易用性损失以不易察觉的方式蔓延”。这种诱惑对存活多代的大众产品尤其强烈——数百万用户提出的上千个功能需求,每一个看起来都是"市场需要它"的证明。而常见的情况是,原结构师已经被嘉奖、调去别的岗位,接手的结构师在均衡表达用户整体利益方面往往经验不足。他引了对 Word 6.0 的批评作例子:功能被打包进去,产品变得又大又慢。

对策是定义用户群。用户群越大越不确定,就越需要明确地定义它,因为每个设计者心里的用户图像都不同,而结构师的图像会有意无意地影响每个决策。要记录的属性有四条:他们是谁;他们需要什么;他们认为自己需要什么;他们想要什么。

然后是方法上最有意思的一步:这些属性实际上是概率分布,而对着尚未清晰定义的对象做调查,既不确定又昂贵。所以他给出的建议是——结构师应该"猜测"或"假设"一整套完整的属性和频率值。理由有三:仔细猜测频率的过程会迫使结构师非常具体地考虑用户群;把它写下来会引发讨论,澄清不同设计者认识上的差异;明确列举频率能让人看出哪些决策依赖哪些用户群属性,这本身就是一种非正式的敏感性分析。他的总结是一句可以直接抄下来的话:“为用户群的属性明确地记载各种猜测。清晰和错误都比模糊不清好得多。”

他还顺手拆掉一个常见的误读。一位敏锐的学生说,《人月神话》自相矛盾地推荐了灾难处方——第 11 章让计划发布新系统的第二个版本,第 5 章却说第二个系统最危险。作者的回答是”他上当了":第 5 章的"第二个"是第二个实际系统,是引入大量新增功能与修饰的后续系统;第 11 章的"第二个"指开发第一个实际系统所进行的第二次尝试,它同样受进度、人员和范围约束。这是语言造成的差异。

WIMP 界面的成功

他把窗口、图标、菜单、指针选取称为几十年中最令人印象深刻的进步,并梳理了它的传播链:1968 年 Doug Engelbart 与斯坦福研究院团队公开提出,Xerox PARC 的 Bob Taylor 团队把它用在 Alto 上,Steve Jobs 先在跑得太慢的 Lisa 上尝试,1985 年在 Macintosh 上商业成功,随后被 IBM PC 兼容机上的 Windows 采用。

他从"人月"的视角解读这次成功:WIMP 是通过类比获得的概念完整性。桌面比喻加一致而细致的扩展——窗口用覆盖而非排列(直接来自桌面);拖放直接模仿用手拾起东西;图标与嵌套文件来自桌面文档;回收站、剪切复制粘贴,甚至把磁盘图标拖进回收站来弹出磁盘。“象征手法是如此的贴切,扩展是如此的连贯一致,新用户常常会被它所体现出的理念打动。”

他接着指出 WIMP 在哪里超越了桌面比喻:菜单与单手操作。真实桌面上人是直接操作文档的,而要求别人做事要发出指令,现有的处理能力无法可靠解释自由形式的命令;界面设计者于是从常用的文档操作里挑出命令,形成类似公文的"便条",让用户在语义标准的菜单里选择。他还分析了"命令表达与双光标问题":命令是祈使句,需要同时指定动词和名词,而自然的方式是两只手各控一个光标——但没有任何商业系统能容纳双鼠标,于是句法被迫改成"先选名词再选动词",光标在数据区与菜单区之间来回奔波,“每一次都丢弃了一些有用的位置信息”。他提出一个更优雅的方案:一只手在键盘上指定动词,另一只手用鼠标指定名词,光标便始终留在数据区。Mac 上把最常用的操作编码到键盘下方的 Z、X、C、V、S,正是这个思路的延伸——快捷键显示在菜单动词旁边,新手可以查,熟练用户直接盲打,且撤销让试错成本极低,于是"从新手向熟练用户的逐渐过渡"得以实现。

最后他点出 Mac 界面真正的高明之处:它用把界面固化进只读内存的方式,让第三方开发者使用标准界面比自造界面更容易、更快——这正是第 6 章推荐的"直接整合"技术的杰出例子。至于 WIMP 的命运,他预言它会在一代之内成为历史:指针选取仍是表达名词的方式,而语音无疑会成为表达动词的方法。

瀑布模型错了,增量开发更佳

这一节是全书最重要的自我否定。他直接写道:"现在我认为这是错误的,并不是因为它太过极端,而是因为它太过简单。“错在哪里?——“未雨绸缪"隐含地假设了传统的顺序/瀑布模型。

他指出瀑布模型的两个基本谬误:其一,它假设项目只经历一次过程,而且体系结构出色易用、设计合理可靠、编码实现能随测试顺畅修改——“换句话说,瀑布模型假设所有错误发生在编码实现阶段”。这导致系统测试甚至用户测试被放在构件过程的末尾,只有投入了全部开发投资之后,才会发现无法接受的性能问题、笨拙的功能和用户意图的错判。其二,它假设整个系统一次性被构建。他还补了一段历史后果:这个模型被奉为军用标准 DOD-STD-2167,以致在多数有见地的从业者已经放弃它之后仍然存活了很久。

因此他强调"必须存在逆向移动”:下游的经验和想法必须跃行而上,有时要跨过一个阶段去影响上游的活动——设计实现发现某些功能定义削弱性能,体系结构就得调整;编码实现发现功能让空间剧增,体系结构与设计实现都得改。在把任何东西变成代码之前,可能已经往复迭代了两轮以上的"体系结构—设计"循环。

替代方案就是增量开发。他引 Harlan Mills 的实时系统做法:先构建一个闭环的框架系统——主循环加一堆空的功能子函数,编译、测试、让它跑起来;然后加上基本的输入输出模块,“瞧,一个可运行的系统出现了”;再一个功能一个功能地开发与增加模块,每个阶段都拥有一个可运行、经过调试和测试的系统,并同样方式做回归测试。在功能都基本可用之后,再逐个精化或重写模块——“增量地开发(growing)整个系统”。

它的收益被他列得很清楚:可以很早就开始用户测试;可以采用按预算开发的策略,彻底保证不会出现进度或预算超支——代价是牺牲允许的功能。他教了 22 年软件工程实验课,四名学生一学期做一套真正的实时系统,后期改用增量开发,“常常因为屏幕上第一幅图案、第一个可运行的系统对团队士气产生的鼓舞效果而感到震惊”。

他还补了两个具体的实践形态。其一是 Parnas 的"产品族”:把软件作为一系列相关产品来设计,预测后续版本的差异,搭一棵家族树,把不易变化的设计决策放在树的根部,使模块的作用最大化;这条策略还能延伸到增量开发过程中间的各个版本,让产品以最低限度的回溯代价生长。其二是微软的"每晚重建":Jim McCarthy 描述说,既然发布后要继续往可运行系统上加功能,那为什么最初的构建要不一样?于是从第一个里程碑开始每晚重建并跑测试,让构建周期成为项目的"心跳"——重建失败就停下整个流程直到问题解决,“在任何时间,团队中的每个人都了解项目的状态”。作者评论说这非常困难、需要大量资源、是规范化且开诚布公的流程,而它给团队的可信度决定了士气和情绪状态。他也记录了一个对照:Bell 北方研究所是每周重建 1200 万行的系统。

至于增量开发与快速原型的关系,他引 Harel 对原型的定义——“仅仅反映了概念模型准备过程中所做的设计决策的一个程序版本,它并未反映受实现考虑所驱使的设计决策”,并指出两者既可独立存在又可相互关联;而"从第一个里程碑开始构建"与快速原型的差别在于功能:里程碑版本可能还不值得任何人使用,而可发布版本在完整性上配备了实用功能集,在质量上能健壮运行。

Parnas 是正确的,我是错误的

这一节的标题直接就是结论。他回顾第 7 章的争论:OS/360 当时的决定是所有人都了解所有材料,每人一份约一万页的工作手册,Harlan Mills 主张"编程是一个开放性的公共过程";Parnas 则相反,认为模块应通过定义良好的接口封装,内部结构是程序员的私有财产。1975 年他站在 Mills 一边。

二十年后他写道:"**但是,Parnas 是正确的,我是错误的。**现在,我确信信息隐藏——现在常常内建于面向对象的编程中——是唯一提高软件设计水平的途径。"

不过他没有一边倒:任何技术用错都会变成灾难。Mills 的技术是让人理解接口另一侧的详细语义,而接口语义的曲解正是系统 bug 的来源;Parnas 的技术则在面对变更时健壮得多,更适合作为"为变更设计"的理念。

顺着第 16 章的判断(过去的进展多来自消除非内在困难,而彻底的进展必须处理根本困难),他认为路径是:承认程序由比语句更大的概念结构要素组成,并尽量只做参数化与组装。他梳理了面向对象的三步源流:Parnas 的模块信息隐藏是第一步,是面向对象的鼻祖;第二步是把模块提升为抽象数据类型;第三步才是继承。而他要强调的关键判断是——"我们希望从面向对象编程中得到的最大收获实际上来自第一步,模块封装,以及预先建成的、为了重用而设计和测试的模块或类库"。他并提醒:这些模块本质上就是第 1 章讨论过的编程产品,很多人想要大规模重用,却不愿付出构建产品级质量(通用、健壮、经过测试和文档化)模块的初始代价——“这种期望是徒劳的”。

Boehm 的数据:人月到底有多少神话色彩

他引 Barry Boehm 对 63 个软件项目的调查(多数为航空项目加 25 个 TRW 项目)与《软件工程经济学》,结论与《人月神话》充分吻合:人力与时间之间的平衡远非线性,用人月作为生产率的衡量标准实际上是一个神话。其中几条值得单独记住:

  • 第一次发布的最优进度时间与估计工作量的立方根成正比(T = 2.5 × MM^(1/3));
  • 计划进度长于最优进度时,成本缓慢攀升——时间越充裕,所花费的时间就越长;
  • 计划进度短于最优进度时,成本急剧升高;
  • 无论安排多少人手,几乎没有项目能在少于 3/4 最优时间的条件下获得成功——他说这条可以充分作为项目经理拒绝不可能的进度担保时的理论依据。

他还引了两项关于 Brooks 法则的研究。Abdel-Hamid 与 Madnick 的结论是:向进度落后项目增加人手总会增加成本,但并不一定总会使项目更加落后;而且由于新成员立刻带来需要数周弥补的负面效应,在项目早期加人比在后期更安全。Stutzke 的模型则在真实项目上做过验证:他在一次中期偏移后成功增加了一倍人手并保住原进度;他最值得注意的一条建议是——后期加入的开发人员必须愿意作为团队成员投入工作,而不是企图改变或改进流程本身。作者也指出这两个模型都没考虑"开发人员必须重新安排"这件事,而实际中这常常是很重要的一步。最后他的态度很稳:“这些细致的研究使’异常简化’的 Brooks 准则更加实用”——但他仍保留那句最简单的陈述,作为最接近真相的经验法则,用来警告经理不要对进度落后的项目采取本能的修补措施。

人就是一切,以及放弃权力的力量

他解释这本书为什么偏重管理而非技术:一半因为他自己在 OS/360 的角色,更根本的是他相信——对项目成功而言,人员的素质、组织和管理比工具或技术方法更重要。Boehm 的 COCOMO 模型支持这一点:团队质量是项目成功最大的决定因素,权重是下一个次重要因素的四倍。他因此说自己欣赏强大工具,但更鼓励对"人的关注、激励与培养"的持续研究,并推荐了 DeMarco 与 Lister 的《人件》——那本书主张"我们行业的主要问题实质上更侧重于社会学而不是科学技术",并给出了工作空间与生产率、缺陷水平之间的关联数据。

他还提醒一件容易被忽略的事:团队融合这种无形的特性极其关键。他观察过六七个项目在不同实验室之间的转移,没有一个是成功的——任务可以转移,但项目转移即使带着良好文档、先进设计和部分原班人员,新队伍实际上仍是重新开始。他判断,正是破坏原有团队的整体性导致了产品雏形的夭折。

接着是本章篇幅不多但语气最热烈的一节:放弃权力。他引 Schumacher 的"附属职能行使"原理——如果较低级别组织的自由和责任得以保留,中心权威实际上是得到了加强,整体反而更融洽繁荣。他记录了两个实践案例:微软的 Jim McCarthy 说每个队伍(30–40 人)拥有自己的任务、进度甚至构建发布流程,争论的仲裁者是团队而不是老板,“我简直无法形容授权和由团队对项目自行负责成功与否的重要性”;IBM 软件业务退休主管 Earl Wheeler 说,把长期集权的管理权力下放之后,“改进的质量、提高的生产率和高涨的士气,这就像是魔术”——团队是流程的所有者,他们有不同的流程,也是进度计划的所有者。作者自己也补了一句谨慎:与成员谈话显示,真正的下放"显得多少有些保守",但授权确实是朝正确方向迈出的一大步。

最令人惊讶的新事物、新产业与元编程

他说每位他交谈过的计算机带头人都承认,对微型计算机革命和它引发的塑料薄膜包装软件产业感到惊讶,这是《人月神话》之后三十多年里最重要的改变。他引 Schumacher 概括的三个目标——价格足够低廉使几乎所有人都能用、适用于小规模应用、满足人们对创造的渴望——指出个人计算机恰好同时满足了这三条。由此他还讲了一个更广的判断:写作、绘画、制图、音乐、摄影等领域进步的原因与软件创造相近,都是消除了次要困难——以前改一稿要重新打一遍,现在改变与评估之间不再丢失思维的连贯性,就像分时之于软件开发。

对软件产业本身,他做了细致的两次划分。传统软件产业仍是四类参与者(计算机提供商、应用程序用户、定制程序开发者、商业包开发者),而 Tom DeMarco 观察到的是整个行业被分解成各个特殊领域:“Ada 是最后一个通用语言,并且它已经慢慢变成了一门专业语言。“与此并行,是成品软件产业——产品以千百万的规模销售,整套软件包的成本可以低于一个支持程序员一天的费用。他注意到这个新产业的文化差异:创业公司行事自由、更关注结果而非流程,天才的个人程序员更容易获得认可,因此"很多新一代的明星人物被吸引到薄膜包装的软件产业”。

由此他给出一个可能比预想更重要的方向:买来开发。提高健壮性与生产率的唯一途径是提升抽象级别,使用模块或对象组合来开发;而特别有希望的趋势是把大众市场软件包当作平台,在上面搭建更定制化的产品(用塑料包装的数据库与通讯软件包做货运跟踪系统或学生信息系统)。他把这种为软件包用户做功能定制的行为称为元编程,并指出它并非新概念(1960 年代就有用汇编宏装备应用语言的专家小组),只是规模大了若干倍——“当我们期待 C++ 类开发的高效市场时,可重用元程序的市场正在悄无声息地崛起”。他判断它处理的确实是根本问题:成品软件包提供了大型功能模块与精心定制的接口,其内部的概念结构根本无需再设计。

他还把这类用户分成四个层次:直接使用用户、在单个应用上做模板或函数的元程序员、向应用添加新功能的外部功能作者,以及把若干应用当作更大型系统构件的元程序员——最后一类需求最没有被满足,也正是收获最大的一类。为此他提出需要一个额外的接口,即元编程接口(MPI):元程序要能在整个应用集合之上控制;软件集必须掌握用户界面;必须能像命令行传参那样调用任何应用的功能;还必须接收应用输出并解析成有类型的逻辑实体。他指出 Unix 的管道与标准 ASCII 文本格式是最早的形态,AppleScript 是优秀的当代例子。

状态与未来

他最后用一个类比收束全书:向化学工程系系主任 Jim Ferrell 请教化学工程与化学的区别,得到的答案是 Arthur D. Little 如何于 1918 年在 MIT 建立第一个工业化学系,专门发现、发展并讲授所有过程共享的技术基础——先是经验法则,接着是经验图表,然后是设计公式,最后是热传导、质量转移与动量转移的数学模型。作者说,近五十年后他仍被化学工程与软件工程发展之间的相似性震动。Parnas 曾说把软件称为"工程"只是一厢情愿,他承认这可能对;但化学工程的历程让他觉得二十七岁的软件工程并非没有希望,只是不够成熟,就像 1945 年的化学工程。

于是第 1 章那三个问题依然悬在最后:如何把一系列程序设计和构建成系统;如何把程序或系统构建成健壮的、经过测试和文档化的产品;以及如何维持对大量复杂性的控制。他的结论毫不轻松——软件工程的焦油坑在将来很长一段时间里会继续使人举步维艰,因为软件系统可能是人类创造中最错综复杂的事物,我们只能在自己力所能及或刚刚超越力所能及的范围内探索。这个行业需要的是持续发展、学会用更大的要素开发、善用新工具、采用经论证的管理方法,以及最后一条:良好的自我判断,和"能够使我们认识到自己不足——上帝所赐予的谦卑”。

我的判断

  • 这一章是全书最有价值的一章,因为它是作者对自己二十年判断的清算,而不是补充论证。它示范了一件罕事:一个人可以既坚持核心主张,又明确宣布自己当年错了——他坚持概念完整性,同时否定了"为舍弃而计划"和"信息隐藏是灾难处方"。
  • 那条否定瀑布模型的自我批评击中了要点:老版本的问题不是结论错,而是隐含假设错(假设项目只走一次、假设所有错误都发生在编码阶段)。这与第 5 章的提醒闭环了——评估任何建议之前先问它的前提是否成立。
  • “结构师像导演、经理像制片人"这个类比是我读到的最好的角色定义之一。它解释了为什么技术决策权与资源决策权必须分开:制片人管预算与档期,导演管作品的一致性;两者都不能替对方做决定。
  • “为用户群属性明确记载各种猜测,清晰和错误都比模糊不清好得多”——这是对这一章"频率猜测"一节最好的概括,也是我读到的最可操作的方法论。注意它的立论不是"猜准”,而是让分歧显性化。
  • WIMP 那一节的分析密度出人意料:他从双光标问题一路推到"键盘管动词、鼠标管名词",再推到快捷键与撤销如何构成新手向熟练用户的过渡通道。这是一段真正的设计推理,比结论本身更有教育价值。
  • 我对他的 WIMP 命运预言持保留:语音输入在今天已普及,但"语音成为表达动词的方法"并没有发生——命名与操作仍然以指针加键盘为主。这说明对交互范式的预测比对工程管理的预测要难得多,因为前者取决于人的社会习惯,而不只是技术能力。
  • Boehm 那几条数据里,我认为"时间越充裕,所花费的时间就越长"最值得单独记住。它意味着排期过宽与排期过紧都是有代价的,而代价的形式不同:前者是成本缓慢膨胀,后者是成本急剧上升。
  • “项目转移没有一个成功"这一条,在我的经验里也成立,只是尺度更小:同一件事在不同环境里重做,即使资料齐全,也会退化成重新开始。它提醒我,能复制的只有产物,不能复制的是共同体。
  • 最后那个化学工程的类比,是全书最诚实的收尾姿态:不宣称方法论已经成熟,也不否认它的前途,只是把软件工程放在一条更长的发展曲线上——“不是没有希望,只是不够成熟”。这比任何豪言壮语都更让人信服。

和我手上的工作有什么关系

  • 关于瀑布模型的那段自我否定,对我的价值是具体的:我做个人项目时最常犯的错,正是把用户测试放在最后——先埋头写完,再拿给别人或自己用,然后发现要的不是这个。增量开发那几条收益里,“可以很早就开始用户测试"和"可以采用按预算开发的策略(代价是牺牲功能)“是我马上可以用上的两条规则。
  • “每晚重建"这个做法我已经有一个弱化版本:每写完一篇笔记就构建、推送、上线。按这一节的说法,它的价值不只在产物,而在于给团队提供可信度——对我而言就是给"进展"提供可信度,它决定了士气与情绪状态。
  • “结构师像导演、经理像制片人"给了我一个判断分工的尺子:在单人项目里我同时扮演两个角色,所以要刻意分清"这次我在做哪个决定”。做概念决策时不该被进度焦虑主导,做进度决策时也不该为了概念完美无限期推迟发布——第 11 章讲的"为舍弃而计划”,恰恰要求这两者在同一张桌子上谈判。
  • “信息隐藏是唯一提高软件设计水平的途径"这句我认同,并且它对我的实际含义很直接:好的接口应该让人不必了解另一侧的实现。我的 bin/build.sh、ocr-pdf.py 正是这样被设计出来的——调用时只需知道它接受什么、产出什么。判断一个抽象是否成功,就看使用它的人是否需要知道内部。
  • “元编程"那一段让我重新看待自己最近做的一件事:这个站点本质上就是以软件包为平台搭建定制产品(Hugo 与 OINK 是平台,我的内容与脚本是元程序)。他也提醒了代价——不能修改平台的内部,且平台不会为你的用法做设计。按这条,我给自己留的自由度应该放在平台的接口边界上(配置、约定、脚本),而不是指望改动平台内部。
  • 最后,“清晰和错误都比模糊不清好得多"这句我打算直接当作这个系列的写作标准:宁可写下可能被推翻的判断,也不要写一段谁都挑不出错但也什么都没说的话。

一句话记住

二十年后作者的清算给出了三条:概念完整性与结构师依然成立;瀑布模型与"为舍弃而计划"错了,因为前提错;而 Parnas 对了——信息隐藏与产品级模块,是通往根本进展的路。软件工程不是没有希望,只是还不够成熟。

12 - 第 2 章 · 人月神话

书名的出处:为什么人月不是可互换的量,为什么向进度落后的项目加人等于用汽油灭火。

这一章的题记来自新奥尔良安托万餐厅的菜单:“美食的烹调需要时间;片刻等待,更多美味,更多享受。” 用菜来类比软件进度,是全文最狠的一处反讽:进度可以被顾客催,但菜不会因为被催就熟。

这一章讲了什么

作者开篇给出判断:在众多软件项目中,缺乏合理的进度安排是造成项目滞后的最主要原因,比其他所有因素加起来的影响还要大。然后列出五个成因:估算技术缺乏有效研究,隐含了"一切都将运作良好"的假设;估算技术隐含假设人和月可以互换,把进度和工作量混为一谈;经理对估算缺乏信心,没有耐心持续估算;缺少进度的跟踪与监督;以及一旦发现进度偏移,下意识的反应是增加人力——“这就像使用汽油灭火一样”。

接着他拆掉乐观主义。所有程序员都是乐观主义者,“这次它肯定会运行”,每一项任务都被假定只需花费它"应该"花费的时间。这里引用了 Dorothy Sayers《创造者的思想》里的三阶段:构思、实现、交流。只有进入实现,才会发现构思的不完整与不一致;而编程的介质又极其易于驾驭,于是我们期待实现过程不会遇到困难——乐观主义正是从这份"介质太顺手"里长出来的。

然后是书名所指的那件事。作者用四张图刻画人员与时间的关系:

任务形态加人的结果
完全可以分解,彼此不需沟通人月互换成立,加人真的能缩短时间
完全无法分解加人对进度毫无帮助
可分解但子任务之间需要沟通最好情况也比不分摊更差
关系错综复杂,需要多人协商沟通开销彻底吞掉分解带来的收益

沟通成本由两部分组成:培训(技术、项目目标、总体策略、工作计划,这部分不可分解,随人数线性增长)和相互交流(若每个部分都要与其他部分单独协作,工作量按 n(n-1)/2 增长)。一对一交流时,3 个人的沟通量是 2 个人的 3 倍,4 个人是 2 个人的 6 倍。作者的结论是:软件开发本质上是系统工作,沟通很快会消耗掉任务分解节省下来的个人时间,添加人手实际上是延长而不是缩短进度。

后续三节处理具体环节。系统测试是进度安排中最不合理的部分,因为需要多少时间取决于缺陷的数量和缺陷难以捕捉的程度,而排期时的假设却是"理论上缺陷应该为零"。空泛的估算用煎蛋作比:顾客要求两分钟上菜,厨师只能把火开大,结果一面焦了另一面还是生的;出路只有两条——用数据(生产率图表、缺陷率图表、估算规则),或者经理挺直腰杆坚持自己的经验判断。最后是全章最著名的推演:

一个估计 12 人月、3 个人做 4 个月、设四个里程碑的任务,两个月后第一个里程碑没有达成。如果只是首段估算不当,剩余 9 人月要在 2 个月内完成,需要 4.5 个人,于是加 2 人;如果整体估算偏少,剩余 18 人月,需要 9 个人,于是加 6 人。作者随即算这笔账的代价:新人要占用一名有经验的老手培训一个月,等于额外投入 3 人月;原来的三部分工作要重新划分为五部分,某些已完成的工作必定丢失;系统测试必然被延长。结果是第 3 个月末还剩 7 个人月的工作,却只有 5 个有效人月——交付依旧延期,和没有加人一样,而团队规模已从 3 人变成 7 人以上,组织和任务划分都变了类型。所以他还给了另外两条出路:重新安排进度(引用硬件工程师 P. Fagg 的忠告"避免小的偏差"),或者削减任务。

我的判断

  • 这条结论常被简化成"加人没用",但作者自己画了图 2-1 作为前提:任务能完全分解且无沟通需求时,人月互换是成立的。所以准确的表述是"当沟通成本超过分解收益时,加人才有害"。判断的边界在沟通结构,不在人数本身——这正是它今天仍然好用的原因。
  • n(n-1)/2 这个模型很粗糙,它假设每一对成员都要单独交流且交流量相同。真实团队的沟通更像分层网络,规模大了会靠结构消解一部分成本。但它的方向是对的:沟通成本的增长快于人数的线性增长。今天的形式从"开会"变成了"同步消息、文档和上下文",曲线本身没变。
  • 我对"持续估算"那条建议存疑。持续估算本身有成本,且需求变动越快估算的保质期越短。更可操作的做法也许是:不追求一次估准,而是让偏差尽早暴露——作者其实也承认进度监督属于另一篇论文。
  • 空泛估算那一节表面在讲技术,实际点在组织:数据能否产生作用,取决于组织是否愿意接受数据带来的坏消息。如果图表只被用来追责,就没有人会认真提供数据。这一层他只用了一句话带过,但比图表本身更重要。
  • 最值得学的是他的论证方式:不谈情怀,直接把 12 人月摊开,一步步算给你看。要让"加人"这个根深蒂固的直觉失效,只有这种算法比直觉更硬的方式才行。

和我手上的工作有什么关系

  • 我现在的开发方式是单人加 Coding Agent 串行推进,天然绕开了沟通成本这一项——只有一个执行者时,n(n-1)/2 等于 0。这解释了为什么这套组合在小到中等规模的任务上效率极高;也标出了它的边界:一旦要靠多个 Agent 并行改同一块代码,沟通成本会以同样的曲线回来,只是形式变成了上下文同步与合并冲突。
  • 系统测试那段对嵌入式项目格外刺耳。硬件相关的缺陷复现成本高、捕捉难度大,而排期时却按"缺陷为零"来算。可行的改法是按经验把测试时间显式写进计划,而不是等它超期后再解释。
  • “避免小的偏差"是我这次读下来最想照做的一条。小延后最容易被默认吸收掉,一路累积到最后就变成无法解释的大延期。
  • 煎蛋那个比喻值得单独记住:进度可以被要求,工作量不会因此改变。当我说"这个两分钟能做完”,需要自查是不是把顾客的期望当成了自己的估算。

一句话记住

人月不是可互换的量:任务一旦需要沟通,新增人力的沟通成本按 n(n-1)/2 增长,很快吃掉分解带来的收益——向进度落后的项目加人,等于用汽油灭火。

13 - 第 3 章 · 外科手术队伍

精干的小队伍跑不完大型系统,一拥而上又做不出概念一致性;Mills 的外科手术队伍是这两难之间的第三条路。

这一章的题记来自 Sackman、Erikson 和 Grant 的测量:“效率高和效率低的实施者之间个体差异非常大,经常能够达到数量级的水平。” 一句话就把"人"这个变量放到了台面上。

这一章讲了什么

作者的起点是一次会议上的常见说法:年轻的软件经理声称,他们喜欢由一流人才组成的小型精干队伍,而不是几百人的大团队——这里的"人"当然暗指平庸者。作者说自己常常有同感,但随即指出,这种幼稚的观点回避了一个很困难的问题:如何在有意义的进度安排内创建大型的系统?

先看测量数据。Sackman 等人的研究里,最好和最差的程序员在生产率上平均相差 10:1,在编程速度和空间占用上有 5:1 的差异;作者据此换算:两万美元年薪的程序员,生产率可能是年薪一万美元者的 10 倍。而经验和实际表现之间没有相互联系。既然个体差异如此之大,一个直截了当的方案就是——既然 200 人的项目里有 25 个最能干的经理,那就开除剩下 175 名程序员,让这 25 个人自己写。

作者立刻自己反驳。这个方案在实践中并不成立:25 人已大到至少需要两层管理(约 5 名管理人员),还要额外的财务、人员、空间、文秘和机器操作支持。更关键的是量级——OS/360 顶峰时有超过 1000 人参与,1963 到 1966 年设计、编码和文档工作花了大约 5000 个人年;如果真能用 200 人做完,需要 25 年。

那么换成理想的小队伍呢?作者做了个慷慨的假设:10 人团队,个个比一般人强 7 倍,再因为沟通少而叠加一个 7 倍因子。5000 ÷ (10×7×7) ≈ 10 年。一个在设计十年后才出现的产品,还有人会感兴趣吗? 于是两难显现:论效率和概念完整性,最好由少数干练的人设计与开发;论交付时间,大型系统需要大量人手。

Mills 的提议是第三条路:大型项目的每一部分由一个团队解决,但团队按外科手术的方式组建,而不是一拥而上。区别在于——不是把问题切开分给每个人,而是由一个人完成问题的分解,其他人向他提供支持。

作者由此给出了 10 人团队的角色清单:

角色职责
外科医生(首席程序员)定义功能与性能技术说明书、设计、编码、测试、编写文档;需要极高天分与大量系统知识
副手后备、思考者、讨论者、评估者;了解全部代码、研究备选设计;是外科医生的保险机制,但对代码不承担具体开发职责
管理员在人员、薪酬、办公空间上拥有决定权,但不在这类事务上浪费时间;小项目可同时服务两个团队
编辑依据外科医生的草稿或口述分析重组,维护多个版本,监督文档生成机制
两个文秘分别服务管理员与编辑,管理非产品文件与项目协作记录
程序职员维护产品库中的技术记录、机器码与可读文件的管理、状态日志与归档
工具维护人员保证基本服务可靠,并负责外科医生所需特殊工具的构建、维护与升级
测试人员既为各功能设计系统测试用例,也为日常调试设计测试数据
语言专家寻找简洁有效的语言用法解决晦涩问题,一人可服务 2–3 位外科医生

Mills 方案真正的关键,作者点得很明确:从"个人艺术"到"公共实践"的观念转换——把所有计算机的运行与产物向全体成员开放,把程序和数据显示为团队的所有物,而非私人财产。程序职员的专业化分工则把程序员从文书杂事里解放出来,同时让那些容易被忽视的杂事也有质量保障。

接下来是比较新旧两种队伍。10 人团队中 7 个专业人士在解决问题,而系统是一个人(最多两个人)思考的产物,于是在客观上达到了概念的一致性。与传统两人队伍的两点区别尤其重要:其一,传统队伍把工作切开,每人负责一部分的设计与实现;外科团队里外科医生和副手了解全部设计和全部代码,既省掉了空间分配、磁盘访问之类的摩擦,也保住了概念的完整。其二,传统队伍成员平等,出现分歧只能讨论和互相妥协,而工作与资源已经分解,分歧就会变成策略和接口上的不一致——比如"谁的空间被拿去做缓冲区";外科团队里不存在这种利益差别,观点不一致可以由外科医生单方面统一。作者的总结是:不分解问题,加上上下级关系,才换来客观的一致性。

最后是扩建。几百人的任务无法直接套用 10 人团队,做法是让 200 人干活,但只协调 20 个"外科医生"——因为每个部分的概念完整性由决定设计的人(原来的 1/7 或更少)负责到底。前提是必须有一个系统结构师自上而下做全部设计,并清晰划分体系结构设计与实现的界线。

我的判断

  • 这一章的论证结构本身就是示范:先立一个直觉(精干小队最好),再用数字把它压垮(10 年),最后给出新结构。比第 2 章更进一步的是,他没有停在"两难"上,而是给出了可操作的分工表。
  • 10:1 那份数据要谨慎对待。作者自己在括号里就写了"我怀疑这种现象是否普遍成立",而且样本是经验程序员的小组研究。用个体差异论证组织结构,风险在于把"人的问题"当成"结构问题"来解决——他后面提出的方案恰恰是靠结构(角色分工 + 单一决策者)而非靠选人。
  • 副手这个角色设计得最精妙也最容易被砍掉:他不承担具体开发职责,只做设计思考、讨论和评审,还要通读全部代码。这等于给关键技术决策配了一个专职的对立面和保险。现实里这个位置常被合并进"技术负责人"自己,于是保险机制名存实亡。
  • 我对"观点不一致可以由外科医生单方面统一"持保留态度。它在概念完整性上收益明确,但代价是把正确性押在一个人的判断上,而且作者没有讨论这位外科医生判断失误时怎么办。第 4 章讲概念完整性时会继续展开这一点。
  • 那份角色清单今天照搬会显得沉重(程序职员、两个文秘、专职语言专家都是 1960 年代的产物),但它的内核仍然锋利:把支持性工作从设计者身上剥离开,让一个人专注于概念,其余人围绕他做放大。今天的形态是自动化工具链和 Agent,而不是文秘和程序职员。

和我手上的工作有什么关系

  • 我现在的开发形态恰好落在这个谱系的极端:1 个"外科医生"加若干个承担支持职能的 Agent——构建、测试、文档、检索都是我之外的自动化角色,我不需要和它们开会,也不存在接口妥协。这一章实际上为这种编队提供了理论依据:支持职能可以很多,但做概念的只能是一个人。
  • “副手"是这套编队里我目前最缺的角色。我的流程里没有专职的评审者,只有事后自己复查。按这一章的思路,成本最低的补法是让一个独立的 Agent 只做设计评审与反例构造,且不给它开发职责——这样它的立场才是对立的。
  • “程序职员"今天对应的是制品管理和可追溯性:版本、构建产物、状态日志、索引。我的站点的 bin/build.sh、CI 门禁、docs/sessions/ 记录,本质上就是在补这个角色,只不过用脚本和仓库代替了专人。
  • 20 个外科医生协调 200 人的比例(1:10)给了我一个粗糙的检查尺度:当一个项目里做概念决策的人超过十分之一,架构一致性就开始危险了。

一句话记住

小队伍输在交付时间,大队伍输在概念一致性;出路不是折中人数,而是换一种编队——让一个人负责全部概念,其余人的职责是放大他,而不是分摊问题。

14 - 第 4 章 · 贵族专制、民主政治和系统设计

概念完整性是系统设计里最重要的考虑因素;它要求设计出自少数人之手,而"谁来当结构师"是个比民主更有效的问题。

题记借的是兰斯大教堂:它的风格"是一种风格上的极致",因为 Jean d’Orbais 构思的整体设计被后继建筑师遵守,“这就是这个宏伟建筑能够如此和谐统一的原因之一”。

这一章讲了什么

作者先用大教堂作对照。欧洲许多大教堂的不同部分风格迥异,因为后来的建筑师总想"提高",以反映自己的品味;而兰斯的和谐来自八代建筑师每个人的自我约束——他们各自牺牲了一点创意,换来纯粹的设计。

对照到软件上,他给出一个判断:计算机系统的概念差异通常不是因为跨越了几个时代,而是因为设计被切分成由若干人完成的若干任务。于是他提出本章的总纲——在系统设计中,概念完整性应该是最重要的考虑因素;宁可省略一些不规则的特性与改进,也不接受一个独立、无法整合的系统,“哪怕它们其实包含着许多很好的设计”。

接着他问四个问题:如何获得概念完整性?这是否意味着结构师贵族专制、压制实现人员的创造性?如何避免结构师写出无法实现或代价高昂的规格?如何与实现人员充分沟通细节?

“获得概念的完整性"一节的答案出人意料地落在易用性上。编程系统的目的是让计算机更容易使用,而代价是软件的外部说明规模达到计算机系统自身说明的 10–20 倍。只有当功能说明节省下来的时间超过学习、记忆和搜索手册的时间,易用性才真的提高。因此他的判断标准不是功能多少,也不是简洁本身,而是功能与概念复杂程度的比值。他举了两个反面参照:OS/360 以功能繁多被开发者推崇,PDP-10 分时系统以简洁精干被建造者称道——“一旦以易用性作为衡量标准,单独的功能和简洁都是不均衡的,都只达到了真正目标的一半”。简洁和直白最终来自一致性:语法上用相同的技巧,语义上有同样的相似性。

“贵族专制统治和民主政治"一节处理那个感情色彩浓厚的问题。前提是硬的:概念的完整性要求设计出自一个人、或者非常少数互有默契的人;而进度压力要求很多人参与开发。作者给出两条出路——仔细地把设计与实现分工,或者采用上一章的外科手术队伍。对非常大型的项目,把体系结构工作与具体实现分离是获得概念完整性的强有力方法:他在 IBM 的 Stretch 与 System/360 上见过它成功,也在 OS/360 上见过它因为没有被广泛采用而失败。

这里他把定义钉死了:体系结构是完整而详细的用户接口说明——对计算机是编程手册,对编译器是语言手册,对控制程序是语言与函数调用手册。体系结构陈述"发生了什么”,实现描述"如何实现”(Blaauw 用时钟举例:表盘、指针、旋钮是结构,里面的机构是实现)。而结构师的角色是用户的代言人:用专业技术知识支持用户的真正利益,而不是维护销售人员或制作者鼓吹的利益。

对民主的质疑,作者的回答分两层。好创意确实经常来自实现人员或用户,但不能与系统基本概念整合的好想法,最好放到一边;如果出现了多个互不兼容的重要构想,正确做法是抛弃原来的设计、合并基本概念、在新基础上重新开始。至于"贵族专制",他的回答是肯定的:必须有人控制这些概念,结构师的工作产物生命周期比实现人员更长,处在解决用户问题的核心地位——“这实际上是一种无需任何(强制的)贵族专制统治”。

“实现人员的创造性被压制"则被他直接归为误解:外部技术说明的编制并不比具体实现更有创造性,只是性质不同的创造工作;在给定体系结构下实现设计,同样需要新的思路与卓越才华,“产品的成本性能比在很大程度上依靠实现人员,就如同易用性在很大程度上依赖结构师”。他进一步反过来说,外部体系结构规定实际上增强了实现小组的创造性:毫无限制的实现小组会把精力耗在结构决策的争论上,对具体实现的关注反而更少。他引 Conway 的说法作证——PL/C 编译器"最终决定支持不经过改进和增强的语言,因为关于语言的争议已经耗费了我们所有的时间和精力”。

最后一节"在等待时,实现人员应该做什么"是全书最坦诚的一段自述。作者回忆 OS/360 外部技术说明的分工会议:体系结构经理有 10 人,需要 10 个月,比允许的进度多 3 个月;实现经理有 150 人,声称可以在体系结构队伍协助下按时交出高质量说明。他把工作分派给了实现队伍,结果两个结论都得到证实——推迟了 3 个月,质量更差,而概念完整性的缺失"至少增加了一年的调试时间"。他点明决定性因素:时间进度,以及让 150 名编程人员有事可做的愿望。

对有人的反对意见"体系结构队伍慢,实现人员只能干等",他的回答是时间顺序问题:在说明完成的时候才雇用实现人员,正如建造建筑的做法。但现实中总要压缩进度,于是他引用 Blaauw 的三阶段——体系结构、设计实现、物理实现——说明它们可以并行。在外部说明完成之前,实现人员有条件做很多事:设定明确的时空目标、弄清运行平台、设计模块边界、表结构、路径与阶段分解、算法与工具,并持续与结构师沟通;物理实现阶段也有库调整、系统管理、搜索排序算法等工作要做。所以,概念完整性"并不意味着该开发模式下的系统需要更长的时间来创建"。

我的判断

  • 这一章是全书论证的枢纽:第 2 章证明了加人有害,第 3 章给出编队,本章回答"为什么必须由少数人决定概念"。三者拼起来才是完整的:人月神话不是关于人数,而是关于概念只能有一处源头。
  • 我最认同的是把"易用性"当作最终标准,而不是把简洁或功能当成标准。这解释了一个常见困境:有的系统功能不多却难用(因为概念不统一),有的系统功能繁多却顺手(因为每条功能都服从同一套原理)。作者给的比值标准,比"少即是多"这种口号可操作得多。
  • 但"不能整合的好想法就放一边"这条代价被低估了。作者假定系统的基本概念是正确且稳定的;如果基本概念本身选错了,这条规则会让整个项目沿着错误方向越走越整齐,而且因为一致性极好,反而更难被质疑。第 5 章讲第二系统效应时会从另一个角度回到这个问题。
  • “外部规格实际上增强实现人员的创造性"这个论断,我认为是有条件的真理。条件写在后面那句里:规定必须"让人把注意力集中在没有人解决过的问题上”。如果规定把实现层面也管死了(细节到算法和数据结构),那就不是解放而是窒息。作者自己也承认第一个反对意见——规格说明里功能太多、成本考虑太少——是"危险的",并把讨论推到了下一章。
  • 那场 OS/360 分工会的自我检讨值得反复看。他明明被说服了,明知结论会错,还是签了——原因不是判断力问题,而是"让 150 人有事可做"的压力。这类决策在项目里随处可见,也是这一章最有现实感的部分。
  • 对"在说明完成时才雇用实现人员",我持保留态度:这在瀑布式的大型硬件项目里可行,但在人员与需求都变动的环境里几乎做不到,更现实的做法是让实现人员早期参与、但明确他们没有结构决策权。

和我手上的工作有什么关系

  • 我自己的工作流里,“体系结构与实现分离"恰好是天然成立的:我先用文档和上下文确定目标与接口(体系结构),再让 Agent 去做实现。这一章给了我一条判据——如果我在结构还没定的时候就放手让它写代码,后面出现的分歧会以返工的形式收账,这一次的 9 倍是隐形的。
  • 结构师是"用户的代言人"这句话,对个人项目也适用:没有人扮演用户时,我很容易替实现方便说话(“这样写更省事”),而不是替使用者说话(“这样用更顺手”)。这个小站从 OINK Starter 一路裁剪到现在,几乎每次返工都发生在后一种声音缺席的时候。
  • “让 150 人有事可做"是我最容易重犯的错误,只是形态变了——我常常为了"让 Agent 别闲着"而并行开多个任务,结果在结构未定的时候产生互相冲突的产出。这一章的教训是:宁可让实现资源等结构,也不要让结构迁就实现资源的空闲。

一句话记住

概念完整性比任何单点功能的优劣都重要,而它只能来自少数人的一致决策;为此宁可让实现人员等结构,也不要为了让所有人都有事做而把设计切成碎片。

15 - 第 5 章 · 画蛇添足

第二个系统最危险:第一次因为无知而谨慎,第二次因为自信而放纵——把第一个系统里搁置的修饰功能全加进去。

题记是奥维德的一句:“聚沙成塔,集腋成裘。“用在功能上,这句话不是褒义——每一小勺都合理,堆起来就是一个没人能用得动的庞然大物。

这一章讲了什么

第一章提出的问题在这里被接着问下去:既然把制定功能规格说明的责任从"快速、低成本地开发产品"的责任中分离了出去,那么用什么准则约束结构师的创造热情?作者给出一个总体回答——结构师与实现人员之间彻底、谨慎、和谐的交流——然后展开两个部分。

第一部分是"结构师的交互准则和机制”。他先拿建筑业打比方:结构设计师用估算技术编预算,承包商的报价总会超过预算,设计师据此修正估算或修订设计,或者建议更省钱的实现方式。计算机系统的结构师处在类似循环里,优势是在设计早期就能拿到"报价”,几乎问了就有答案;劣势是常常只剩一个承包商,而后者可以用抬高或压低估计来表达对设计的好恶。所以尽早交流、持续沟通的价值是双向的:结构师获得成本意识,开发人员获得对设计的信心,而各自的责任分工不被混淆。

当估算过高时,结构师只有两条路:削减设计,或者改用成本更低的实现方法。后一条是在挑战开发人员的做事方式,要成功必须守住四条准则:

  • 牢记实现的责任由开发人员承担,结构师只能建议,不能支配;
  • 为指定的说明主动建议一种实现方法,同时准备接受任何其他能达到目标的方法;
  • 对这种建议保持低调、不公开;
  • 准备放弃自己坚持的改进建议。

作者的提醒很关键:一般开发人员会反对体系结构上的修改建议,而且通常他是对的——因为在实现层面,某些次要特性的改动会带来意料之外的开销。

第二部分"自律——开发第二个系统所带来的后果"是本章著名的诊断。第一个系统里,结构师知道自己对任务了解不够,所以谨慎、仔细,倾向于精炼和简洁;那些冒出来的装饰与润色功能,被他搁置在一边,标记为"下一个"项目的内容。第一个系统结束之后,他信心十足、精通这类系统,于是着手第二个——而第二个系统是设计师们所设计的最危险的系统。普遍倾向是过分设计:把第一个系统里小心搁置的想法全部加进去。到第三、第四个系统时,先前经验互相验证,才会形成对通用特性的判断。

书里给的例子一个比一个沉重。IBM 709 是对成功的 704 的二次开发,操作集合设计得如此丰富,只有一半操作被客户经常使用。更严重的是 Stretch 计算机:它是很多人被压抑的创造力宣泄口,也是他们中大多数人的第二个系统,Strachey 评审时的评价是——“它非常富有创造性,设计非常复杂,却非常高效。但不知为什么,我同时感觉到,它粗糙、浪费、缺乏优雅,并让人觉得必定存在某种更好的方法可以代替它”。

OS/360 对大多数设计者来说是第二个系统:团队成员来自 1410-7010 磁盘操作系统、Stretch 操作系统、Mercury 实时系统和 7090 的 IBSYS,几乎没有人有两次以上的操作系统经验。所以作者称它是"软件行业的 Stretch 系统",Strachey 的赞誉和批评可以一字不改地套用。随后是一串具体病例:

病例问题所在
26 字节常驻日期翻转例程为正确处理闰年的 12 月 31 日写了常驻代码,其实完全可以留给操作员
链接编辑器的覆盖(overlay)管理静态覆盖技术的顶峰,但系统已经是多道程序、动态内核分配,概念上正相冲突;它还占空间、拖慢链接,讽刺的是链接的目的本是避免重新编译——“像一个挺着大肚子的节食者”
TESTRAN 批调试程序快照与转储功能优雅,但"无需重编译的批调试"整个概念已被交互式系统与快速编译挑战
OS/360 调度程序管理固定批作业非常杰出,对远程任务项、多道程序与永久驻留交互式子系统却几乎毫无帮助,实际上让它们更难

这里有一个容易被忽略的观察:第二系统的表现不只是"加功能",还有一种更隐蔽的形式——对某些已经被淘汰的技术做细化与精炼。作者的判断很狠:“直到系统的思想已经十分优越时,才开始对原有技术进行细化和精炼。”

那么怎么避免?结构师跳不过第二个系统,但可以有意识地警惕它,运用特别的自我约束准则,并根据系统基本理念与目的的变更舍弃一些功能。他给出一个很实在的准则:为每个小功能分配一个值——“每次改进,功能 x 不超过 m 字节的内存和 n 微秒”。这些数字在一开始充当决策向导,在物理实现期间成为指南和对所有人的警示。至于项目经理,作者的要求是确保由至少有两次以上系统经验的资深结构师来做设计,同时保持对特殊诱惑的警觉,不断提出正确的问题,确保原则与目标在详细设计中得到完整体现。

我的判断

  • 这是全书最容易对号入座的一章。它揭示的其实是学习曲线上的一个陷阱:第一次因为无知而谨慎,第二次因为过度自信而放纵。第一次的克制不是修养,而是不安全感;所以当不安全感消失时,克制也随之消失。
  • “为每个小功能分配 m 字节、n 微秒"是这一章最可操作的处方。它比"要克制"“要有品味"这类劝告硬得多——把审美问题转成了预算问题,而预算是可以被质询、被拒绝的。这条我认为可以直接搬出软件范围使用。
  • 我怀疑"第二系统"这个框架的边界。作者的样本全是超大型系统,且发生在"一个设计师一辈子做两三个系统"的年代。今天一个技术负责人一年可能做五个系统,第二系统效应会被高频迭代稀释;但它也可能以更隐蔽的形式出现——每一次技术栈迁移或重写,都是一次"我终于可以把它做对"的放纵机会。
  • 那些病例的共同结构是:局部最优的技术精炼,撞上已经改变的系统基本假设。所以它给了我一条可复用的检查动作——评估任何改进之前,先问"它依赖的基本假设现在是否还成立”,而不是问"它本身好不好”。
  • 这章末段的中译有一处歧义:“他必须坚持至少拥有两个系统以上开发经验结构师的决定”。对照上下文,我理解为"坚持让有两次以上系统经验的资深结构师来做设计";若按字面读成"服从结构师的决定",就会与上一段"根据基本理念及目的变更,舍弃一些功能"冲突。读书时遇到这种句子,回看原页确认是值得的。
  • 最难得的是那句"开发人员反对结构修改建议,通常他是对的"。它等于要求结构师主动接受自己的建议被否——这在一个以品味和远见为荣的角色上,是很高的自律要求。

和我手上的工作有什么关系

  • 我手上的东西很多本身就是"第二系统":这个站点是对 OINK Starter 的二次开发,而 Starter 是别人的第一次。区别在于我有一份明确的约束清单——AGENTS.md 的硬性规则、构建门禁、.gitignore 的排除范围——它们实际承担了"为每个功能分配预算"的角色。没有这些约束,二次开发膨胀成"什么都想要"是必然的。
  • 顺着这一章,我打算给主题定制立一条可执行的规矩:任何视觉改动必须写明它替换掉了什么,不允许净增。这就是"m 字节、n 微秒"在内容站上的等价物。
  • “局部最优撞上已变假设"那组例子,几乎是为我的技术栈迁移史写的:花力气优化过的东西,常在半年后因为架构变化而作废。以后决定优化前,先问一句这条优化依赖的假设还在不在。
  • “结构师只能建议不能支配"对我和 Agent 的协作有直接含义:概念与结构的决定权在我,具体实现的选择权应当交还给执行者。反过来,当我为某个实现方案辩护时,要检查自己是不是已经越界到实现层了。

一句话记住

第二个系统最危险:第一次因无知而谨慎,第二次因自信而放纵,把当初搁置的修饰一股脑加上去——可靠的解法不是提醒自己谦虚,而是给每个功能一个明确的预算。

16 - 第 6 章 · 贯彻执行

结构师的决策如何让一千个人听到、理解并照做:手册、形式化定义、周例会、多重实现、电话日志与产品测试。

题记引的是杜鲁门论总统权力:“他只是坐在那里,嘴里说’做这个!做那个!‘当然,什么都不会发生,光说不做是没有用的。”

这一章讲了什么

问题很具体:假设项目经理已经有了行事的规范、经验丰富的结构师和一批实现人员,他如何确保每个人听到、理解并实现结构师的决策?一千人开发的系统,十个结构师的小组怎么保住概念完整性?这一章给的是 System/360 硬件设计中摸索出来的六件工具。

手册是基础。它是产品的外部规格说明,描述并规定用户可见的每一个细节,也是结构师的主要工作产物。但仅有文档不够——规格会被反复修改,所以修改必须阶段化,进度表上要有带日期的版本。手册要写全所有用户可见的界面,同时避免描述用户看不见的东西,因为那是实现人员的创造空间;结构师可以为自己描述的特性建议一种实现方法,但不能试图支配实现过程。风格上要求清晰、完整、准确,每条说明重复所有基本要素并彼此一致——“这往往使手册读起来枯燥乏味,但是精确比生动更加重要”。一个惊人的细节:System/360 的 Principles of Operation 文字上的一致性只来自两名作者(Gerry Blaauw 和 Andris Padegs),想法是大约十个人的,但转写成书面规格必须由一两个人完成。作者特别称赞 Blaauw 写的那份兼容性附录,因为它不只定义了规定什么,还定义了未规定什么。

形式化定义是第二种工具。人类语言不精确,形式化标记精确且倾向完整,“差异越明显,填补得越快”,代价是不易理解;记叙性文字能表达原则、层次、实例、异常,最重要的是能解释原因。所以他的结论是两者并用,但必须明确以谁为标准:Algol 68 以形式化定义为准,PL/I 以记叙性文字为准,System/360 也是记叙性为准、形式化作为派生论述。他还专门警告,形式化定义几乎总会滑向描述某个设计实现——而实现规定了太多东西。

这条警告接着被推到一个更尖锐的形态:把实现本身当定义。做法是"新机器与原有机器一致",遇到手册模糊处就"问一问机器"——写段程序测出行为,新机器照此运行。优点是所有问题都能靠试验得到迅速而精确的答案,从不需要争辩;缺点压过了优点:实现会过度规定外部功能。他举的例子很妙——在 System/360 上模拟 IBM 1401 时,有 30 个被认为无效操作的副作用被广泛使用,最终成了定义的一部分;而这类"粗糙"的功能在别的实现里往往低效或代价高昂(比如乘法运算后把垃圾留在被乘数寄存器,一旦成为定义的一部分,就会阻止某些快速乘法算法)。用实现当标准,还必须禁止对实现的任何修改。

直接整合是给软件结构师的一种更可爱的方法:为模块间接口设计声明(参数或共享存储器),要求实现在编译期包含这些声明——PL/I 的宏或 %INCLUDE 就是这种机制。接口只通过符号名称引用,于是修改声明时只需增删变量、重新编译,使用方程序不必改动。

会议分成两级。周例会是每周半天,所有结构师、硬软件实现人员代表和市场计划人员参加,由首席系统结构师主持:建议书通常会前以书面形式分发,会上重点在创新而不只是结论,小组先设法找出各种解法,再把少数方案交给结构师写成正式的变更建议;随后对建议做决策,往返几轮,正面与负面意见都被完整描述。达成共识最好,达不成由首席结构师决定。作者列了这套机制有效的五个原因:同一个小组每周交流、不需要额外培训;成员都与产品相关、没人扮演"顾问"、每个人都承担义务;问题出现时同时在界线内外找方案;正式书面建议强制了决策、避免了草稿纪要的不一致;以及明确授予首席结构师决策权,避免妥协和拖延。

年度大会则是给"堆积起来的小事"准备的:一些决定没贯彻好,一些小事情没被真正接受,有些决定引来新问题,而周例会又不愿重新翻案,于是不满慢慢堆积。大会在手册冻结前夕开,持续两周(作者说若由他重排会改成每半年一次),出席者包括体系结构小组、编程与实现人员代表、编程经理、市场和实现人员,由项目经理主持,议程约 200 条。通过出色的计算机化文本编辑,每天早晨与会者在座位上拿到已更新的手册,记录前一天的决议。作者称它为"收获的节日":不仅解决决策问题,还让决策更容易被接受——每个人都在倾听、参与,对复杂约束之间的相互关系理解更透彻。

多重实现是 System/360 结构师独有的优势条件之一。大多数项目里,机器和手册迟早会出现不一致,而人们通常忽略手册——因为改手册比改机器便宜。但当同一份规格被多个独立实现同时遵守时,天平反转了:让机器去符合手册的成本,反而低于让手册去迁就机器。所以多实现之间的兼容性要求,是"强制规格说明的最佳代言人"。同理,定义编程语言时如果起初就有两种以上实现,定义会更整洁规范。

电话日志处理规格说明无法穷尽的解释问题。作者的要求很朴素:鼓励有疑问的实现人员打电话问结构师,而不是一边猜测一边干活;同时答案必须是能告知所有人的权威结论。机制是结构师自己记录每个问题和回答,每周把若干人的日志合并、整理、分发给用户和实现人员——不正式,但快捷易懂。

最后是产品测试。作者的开场句很漂亮:“项目经理最好的朋友就是他每天要面对的对手”——独立的产品测试小组。它按规格说明检查程序,充当麻烦的代言人,专门找出未贯彻执行的地方、被误解的设计决策。每个开发机构都需要这样一个独立的技术监督部门以保证公正,因为最终用户才是那个独立监督者,而测试小组是顾客的代理人。它的结论是:测试小组不只是质量关口,更是让设计决策得以贯彻的必要手段,因此必须尽早着手、与设计同步实施。

我的判断

  • 这一章把前四章的抽象论证落地成了机制,我读下来最有价值的一点是:概念完整性不是靠反复宣讲维持的,而是靠结构强制维持的。手册的阶段化版本、形式化与记叙性的主辅分工、多重实现带来的兼容压力、电话日志的单一权威答案,这些全是把"共识"变成"约束"的手段。
  • “定义兼容性时还必须定义未规定什么”,这句话超出了软件范畴。绝大多数接口文档只写"支持什么",读的人无法判断某个边界行为到底是被承诺的还是碰巧的。作者那句话等于要求作者承担消极定义的责任,这是很少见的严格。
  • 我特别认同"把实现当定义"的批评,因为它在今天极其常见:接口行为以现有实现为准,测试用例成为事实上的规范。作者点出了代价——无效操作的副作用会被用户当成契约,而一旦成为契约,某些更好的实现方式就被永久堵死。这是技术债里最难还的一种。
  • 周例会那五条有效原因里,最容易被忽略的是"没有人是顾问的角色,每个人都要承担义务"。参与决策的人与承担后果的人必须是同一批,否则会议就会退化成意见征集。
  • 我对年度大会这种形式持保留:两周、200 条议程、项目经理主持,只在超大项目里成立。但它的内核——给"堆积起来的不满"一个定期清算的场合——非常值得移植到小团队,我自己的做法是把它压缩成定期回顾,只是很容易因为不急而取消。
  • 产品测试被定义为"顾客的代理人",而不是"质量检查员"。这个身份差异很关键:检查员对流程负责,代言人对用户负责,后者才有资格质疑"我们是不是做错了"。

和我手上的工作有什么关系

  • 这个站点现在的 AGENTS.md 与 docs/reading-protocol.md 扮演的就是"手册"的角色:把稳定的约定集中在一处,让每次协作不必重新解释。这一章提醒我,手册必须阶段化——所以我把关键决定同时记进 docs/sessions/,让约定能追溯到"哪一天、因为什么"变成这样。
  • “直接整合"这条我有现成对应物:站点用 bin/build.sh 作为唯一构建入口,CI 与 Cloudflare 都调它,改一处即全局生效。这就是接口只通过符号名称引用的效果,避免三份配置各自漂移。
  • 电话日志的现代等价物是会话记录。作者要求结构师把每个问题和回答记下来、每周合并分发;我把 docs/sessions/ 当作这个日志用,它解决的是同一个问题——同一个人重复被问同一件事。
  • 多重实现那条给了我一个可执行的启发:同一份内容如果有两个独立消费者,规范就会被逼着变清楚。我的站点每页同时产出 HTML 与 Markdown 供人和 Agent 读取,这个双出口本身就是一种兼容性压力,会不断暴露表述含混的地方。
  • 我最缺的是"顾客的代理人”。目前没有独立测试环节,只有我自己事后复查——按这一章的判据,这意味着很多设计决策是否真的被贯彻执行,其实无人验证。

一句话记住

决策被听见不等于被贯彻;靠手册的统一文字、多重实现的兼容压力、会议上的明确授权、日志里的权威答案,加上一个专找麻烦的测试小组,概念才真的落到产品里。

17 - 第 7 章 · 为什么巴比伦塔会失败

目标清晰、人力充足、材料齐备、时间不限、技术可行——巴比伦塔仍然失败了。缺的是交流,以及由交流产生的组织。

题记直接引了《创世纪》11:1–8 的原文。这一段之所以被放在这一章,是因为作者要把它当作一个工程项目来复盘。

这一章讲了什么

作者称巴比伦塔是人类继诺亚方舟之后的第二大工程壮举,也是第一个彻底失败的工程。他逐条检查这个项目的先决条件:

先决条件答案
清晰的目标?有,尽管幼稚得近乎不可能——而且项目早在遇到这个限制之前就失败了
人力?非常充足
材料?美索不达米亚有丰富的泥土和柏油沥青
足够的时间?没有任何时间限制的迹象
足够的技术?有,金字塔或锥形结构本身稳定、能分散压力负载,砖石建筑技术有过深入研究

五个条件全都满足,项目还是失败了。作者说他们缺两样东西:交流,以及交流的结果——组织。无法相互交谈,就无法合作;合作无法进行,工作就停顿。而在史书的字里行间能看出来:交流的缺乏导致争辩、沮丧和群体猜忌,部落开始分裂——大家选择了孤立,而不是互相争吵。

现实完全对应得上。左手不知道右手在做什么,于是进度缓慢、功能不合理、系统缺陷一一出现。作者举了一个极具体的例子:程序覆盖功能的实现者发现应用程序很少用到这个功能,于是降低了它的速度;与此同时,队伍里其他人正在设计在很大程度上依赖覆盖速度的监控程序——覆盖速度的变化于是成了主要的规格变更。这类变更必须从系统角度衡量,并且公开、广泛地发布。

那么团队靠什么交流?他给了三条途径:非正式途径(清晰定义小组内部关系、充分利用电话,形成对所写文档的共同理解)、会议(常规项目会议,各团队轮流做简要技术陈述,能澄清成百上千的细小误解),以及工作手册。

“项目工作手册"是本章篇幅最大的一节。作者先纠正一个误解:它不是一篇独立文档,而是对项目必须产出的一系列文档进行组织的结构——目的、外部规格说明、接口说明、技术标准、内部说明和管理备忘录,全部必须归属于这个结构。

为什么需要一个结构?一是技术说明几乎必不可少,而结构保证文档规范而不是杂乱无章,后来的文字能找到它该在的位置;二是控制信息发布——“控制信息发布并不是为了限制信息,而是确保信息能到达所有需要它的人的手中”。做法是先给所有备忘录编号,更好的是用树状索引结构,并让子树维护各自的发布列表。作者点出规模效应:10 人的项目简单编号就够,100 人的项目若干线性索引常常够用,到了 1000 人且分散在多地,结构化手册的必要性和规模要求都陡增。

OS/360 的实践细节很值得看。他们决定每个编程人员都要了解所有材料,因此每间办公室保留一份工作手册拷贝;用活页夹替换变更页来实现实时更新,靠计算机文本编辑把周转时间压到一天以内。变更页要满足两种追问:第一次收到时要知道"修改了什么”,事后咨询时要知道"现在的定义是什么"。所以变更页上要用页边竖线标记发生变动的行,并附一段简短的独立说明,记录变更的重要性与批注。

这套机制稳定运行了六个月,然后被自己的重量压垮:手册厚达五英尺,一百份叠起来比 Time-Life 大厦还高;每天分发的变更页约两英寸厚,归档约 150 页,日常维护占据每个工作日的大量时间。他们改用微缩胶片,体积减少 18 倍,省下大量金钱。但作者没有把微缩胶片当万能药,他诚实地列出代价:正是因为文字归档笨拙,才确保了所有变更都被阅读,而微缩胶片让维护过于省事,除非变更说明与变更胶片一起分发;而且读者无法在胶片上做标记和批注。他的结论是:微缩胶片是好方法,但应作为文字工作手册的补充。至于未来的方向,他提到了可直接访问的文件——从显示终端查阅、修订日期与变更标识条、每日小结按后进先出保存,并点名 Engelbart 等人在 ARPA 网络项目中的实践,以及 Parnas 提出的更彻底的方案。

最后是"大型编程项目的组织架构"。他先算了一笔交流账:n 个工作人员之间有 (n²−n)/2 个相互交流接口,还有将近 2n 个必须合作的潜在团队。团队组织的目的是减少所需的交流与合作的数量,手段是人力划分与限定职责范围——树状管理结构之所以出现,是因为管理角色不可重复,它本质上是权力和责任的结构。但作者提醒:交流的结构并不受树状约束,交流是通过网状结构进行的。

他给了每棵子树必须具备的六个要素:任务、产品负责人、技术主管(结构师)、进度、人力的划分、各部分之间的接口定义。其中两个角色需要分清:

  • 产品负责人:组建团队、划分工作、制定进度表,争取并保证资源;主要工作是向上的沟通和水平的沟通;建立团队内部的沟通与报告方式;确保进度目标实现,并根据环境变化调整资源与团队架构。
  • 技术主管:构思设计、识别子系统、指明外部样貌、勾画内部结构;提供设计的一致性与概念完整性;控制系统复杂程度;解决技术问题或调整设计。他的沟通在团队中是首要的,工作几乎完全是技术性的——作者借用一句谚语,称他为"攻坚小组中的独行侠"。

两者的关系有三种,作者说三种在实践里都成功过。第一种是同一个人兼任,适合 3–6 人的小队;大型项目里很难,因为兼具两种技能的人极稀缺(“思考者很少,实干家更少,既是思考者又是实干家的太少了”),而且两个角色在大项目里都要全职甚至加班。第二种是产品负责人当总指挥、技术主管当左右手:难点在于技术主管不参与管理时如何建立技术权威;所以产品负责人必须预先声明其技术权威、在绝大多数测试用例上支持他的技术决定,两人还要在基本技术理论上观点相近、在主要技术问题出现前先私下讨论。作者甚至提到要用办公室大小、地毯、装修、复印机这类微妙的状态特征去暗示技术主管的威信——“尽管他身在管理团队之外,但他是决策的根源”。这种组合很有效,却很少被采用,好处是能用上不擅长管理的技术天才。第三种是技术主管当总指挥、产品负责人当左右手,作者引了海因莱因《出售月球的人》里的一段:Harriman 把总工程师 Coster 从卡车合同和客户电话里隔离出来,给他换办公室、加沙发,再配一个"除了建造飞船的微妙细节外什么都不用你担心"的下属。

他的结论是分场景的:对小团队,第三种最好(这正是第 3 章的外科手术队伍);对真正大型项目中的一些开发队伍,产品负责人作为管理者更合适。全章收在一句话上——巴比伦塔可能是第一个工程上的彻底失败,但不会是最后一个:交流,以及交流的结果——组织,是成功的关键;积累这方面的经验和能力,同提高软件技术本身一样重要。

我的判断

  • 这一章与第 6 章构成一对:第 6 章讲决策如何向下贯彻(手册、会议、日志),本章讲信息如何在横向流动(工作手册、组织架构)。两章合起来才是"交流"的完整问题,作者把它们分开处理是对的。
  • 最锋利的是那个"降低覆盖速度"式的例子:局部最优的调整,在系统层面造成规格变更。它揭示的不是某个人粗心,而是缺少一个制度动作——变更必须被系统性地评估与发布。这一点至今仍是最常见的工程事故来源。
  • “控制信息发布不是限制信息,而是确保信息到达需要它的人"这句话,我完全认同,且值得当作文档系统的设计目标。它解释了为什么"把所有东西都放进一个共享盘"并不等于信息通畅:没有结构,等于没有发布。
  • 作者对微缩胶片的评价是我判断他诚实的证据:他既承认它省了钱和体积,也承认笨拙本身就是一种保障,微缩胶片让维护太容易,反而可能没人读变更说明。这种"改进会削弱原有隐式约束"的观察,比结论本身更有价值。
  • 我保留的是"三种关系都成功过"的说法。它可能意味着三种都能用,也可能意味着成功的样本被事后解释成了某种关系;作者没有给出判断依据。我个人更认同他最后的分场景结论,而不是"都可以”。
  • 六个基本要素里,最容易被漏掉的是接口定义。任务、负责人、结构师、进度、分工这五样在项目启动时几乎总会被写上,接口定义却常被默认成"以后自然会对齐",而它恰恰是巴比伦塔真正缺的那一块。

和我手上的工作有什么关系

  • 我自己最常犯的错,正好对应本章开头那个先决条件表:目标清晰、时间够、技术可行、工具齐备,于是默认"当然能做成"。少数几次翻车都不是技术问题,而是我和昨天、上周的自己失联——也就是左手不知道右手在做什么,只不过两只手都是我。
  • 工作手册的思路,直接解释了我为什么坚持维护 AGENTS.md 与 docs/sessions/:前者是索引结构(相当于树状索引),后者是变更页与归档。这一章还给了两条可操作的改进:给每次变更标记"改了什么",以及随时能回答"现在的定义是什么"。
  • “每个编程人员应了解所有材料”,在小规模协作里的等价物是让每个参与者都能拿到全部上下文,而不是按角色切分信息。这一条我打算在站点里继续贯彻:约定文档公开可读,会话记录可追溯。
  • 产品负责人与技术主管的组合分析,对"我 + Agent"这种编队有直接映射:我兼任两者,属于作者说的第一种安排——他只推荐给 3–6 人的小队,这正好是小规模配合 AI 的团队规模。超过这个规模,就该有人专职做概念、有人专职做交付。
  • 接口定义这条我准备马上用起来:给这个站点补一份"约定清单",把页面结构、front matter 字段、锚点规则、构建入口全部写明。以前这些散在几次对话里,正是"以后自然会对齐"的典型形态。

一句话记住

目标、人力、材料、时间、技术全都齐备的工程照样会失败——缺的是交流,以及交流产生出来的组织;而组织的第一份产物,就是把一切文档结构化的工作手册。

18 - 第 8 章 · 胸有成竹

系统编程到底要多久?工作量是规模的 1.5 次幂,生产率随交互密度与任务复杂度差出十倍,而高级语言能换来 5 倍。

题记两句话放在一起很有意思:普布里乌斯说"实践是最好的老师",《穷理查年鉴》补上一句——“实践是最好的老师,但智者还能从其他的地方有所收获。“这一章就是作者试图从别人的实践里收获的部分。

这一章讲了什么

开篇两个警告,都是针对估算里最常见的偷懒方式。

第一,不能只估编码再套比率。编码只占整个问题的六分之一左右,编码估计或比率上的误差,放大到整任务上就是荒谬的结论。

第二,不能拿小型独立程序的数据外推。他引 Sackman 等人的报告:规模平均 3200 条指令的程序,单个程序员编码加调试约 178 小时,外推得到每年 35800 条语句;而规模只有一半的程序,花费时间大约只有前者的四分之一,推出来的生产率接近每年 80000 行。“计划、编制文档、测试、系统集成和培训的时间必须被考虑在内”,所以这种外推毫无意义——他用了一个很妙的类比:“就好像把 100 米短跑记录外推,得出人类可以在 3 分钟之内跑完 1 英里一样。”

把这两条警告说清楚之后,他给出第一条实证规律:工作量是程序规模的幂函数。图 8-1 引的是 Nanus 和 Farr 在 System Development 公司的研究,指数约为 1.5:

工作量 = 常数 × 指令数量^1.5

Weinwurm 在 SDC 的另一项研究给出接近 1.5 的指数。这条规律意味着规模翻倍时工作量增长到约 2.8 倍——和"人月可以按规模线性摊"的直觉直接冲突。

接下来五组数据从不同角度逼近真实生产率。

Portman 的数据解释了一类系统性偏差。他发现自己的队伍落后进度约一半,每项工作实际耗时约为估计的两倍;而那些估计是很有经验的团队用 PERT 图对人小时细分到数百个子任务做出来的。他要求团队记录时间日志,答案立刻出现:队伍只用了 50% 的工作周做实际的编程和调试,其余时间花在机器当机、高优先级的琐碎工作、会议、文字工作、公司业务、疾病和事假上。也就是说,估算对"每个人年里有技术工作时间"这件事做出了不现实的假设。

Aron 的数据按程序员之间的交互密度分类,给出三个量级差别:

交互程度生产率(指令/人年)
非常少的交互10000
少量的交互5000
较多的交互1500

这些数字只含设计与编程,不含支持和系统测试;作者说把它们除以 2 以计入系统测试,结果与 Harr 的数据非常接近。

Harr 的数据来自贝尔电话实验室电子交换系统,四个程序——两个基本控制程序、两个基本语言翻译程序。生产率的单位是"经调试的指令/人年”,已包含编程、构件测试和系统测试。结果分成鲜明两档:控制程序约 600 指令/人年,语言翻译程序约 2200。四个程序规模相近,差别只在工作组大小、时间长短和模块个数上。作者在这里表现得很克制:因果无法确定——“是控制程序确实更复杂,所以需要更多人?还是因为被分派了过多的人,所以需要更多模块和更多人月?没有人可以确定。“但他强调数据反映的是真实状况,因此 Harr 的贡献是实实在在的。

OS/360 的数据给出同一量级的独立佐证:控制程序组约 600–800 经调试指令/人年,语言翻译组 2000–3000。作者由此提炼出一条粗略的换算原则——编译器的复杂度是批处理程序的 3 倍,操作系统的复杂度是编译器的 3 倍。

最后是 Corbato 的数据,也是全章唯一让人振奋的一处。MIT 的 MULTICS(约 100–200 万条指令)报告显示平均生产率为 1200 行经调试的 PL/I 语句/人年。换算时要注意单位差异:每个语句对应手写代码的 3–5 条指令,于是得到两条结论:

  • 对常用编程语句而言,生产率似乎是固定的(这个数字已经包含了注释与可能的错误在内);
  • 使用适当的高级语言,编程生产率可以提高 5 倍。

我的判断

  • 这一章性质上是一份数据综述,而不是论证。放在第 7 章之后是有意的:先把"交流与组织"讲透,再谈"要多长时间”,顺序上先讲清楚会决定工作量的因素,再谈量化,避免把估算做成纯数字游戏。
  • 1.5 次幂这条规律是本章最重要的结论,也是最容易被今天的人忽略的。它给出的是非线性:规模翻倍、工作量约 2.8 倍。作者没有解释指数为什么是 1.5,我读到这里的理解是,它把接口、集成与测试随规模增长的速度一并吞进去了——这也解释了为什么它和"复杂度三级跳”(批处理 → 编译器 → 操作系统)的量级判断是自洽的。
  • Portman 的数据今天依然成立,而且很反直觉:估算错一倍,往往不是估错任务,而是估错了可用时间。一半的工作周被非技术事务吃掉,这种偏差在任何按"人年"排的计划里都会隐身。
  • Harr 数据里那个因果不确定,我认为是作者全书最诚实的一段:同一份数据既支持"复杂任务需要更多人”,也支持"派了太多人导致模块变多",他明确说没人能确定。可贵的不是数据,而是他没有借数据去证明自己的观点。
  • Corbato 的"高级语言提高 5 倍"这条结论要小心。它是 1970 年代从汇编到 PL/I 的跨越,今天已经不存在同等量级的代差;把它当成"换更高级的语言就能再提 5 倍"来用,是误读。真正仍然有效的是前半句:对常用语句而言生产率是固定的——也就是说,逐行写的效率很难提高,要提高只能改变"要写多少行"。
  • 我对"编译器 = 3 倍批处理,操作系统 = 3 倍编译器"这条保持怀疑:它更像经验秩序而非测量结论,而且三级跳的边界在任何时代都是主观的。但它作为一种迅速对齐预期的手段仍然有用,前提是不要把它当精确系数。

和我手上的工作有什么关系

  • 我自己最容易犯的错正是本章开头警告的第一条:按"写代码的时间"推算整件事的周期,却漏掉设计、文档、测试、集成与学习成本。按这里的比例,编码只占六分之一——这与我实测的体验接近,只是我以前没有把它写下来当预算依据。
  • Portman 的 50% 对我尤其有用。它给了"为什么感觉没做什么事情一天就过去了"一个量化解释,也提示我在排期时应该按可用技术时间的比例打折,而不是按日历时间乘人数。
  • 1.5 次幂这条,我打算把它用成一个预警指标:当项目规模翻倍时,如果计划只涨了一倍工作量,就该怀疑接口与集成成本被漏算了。对个人项目也一样——加一个栏目、加一门语言,代价不是线性的。
  • “生产率是固定的,要提高就得减少要写的行数"这条结论,直接支持了我现在的工作方式:与其纠结手写的效率,不如把机械的部分交给脚本、模板与 Agent(bin/build.sh、archetypes、批量 OCR 都是我实践这条的产物)。
  • Aron 那三档交互密度给了我一个判断工具:同一件事,在"低交互"和"高交互"之间的生产率能差 6 倍以上。所以当我觉得某个任务特别慢时,先问的不是"我是不是变笨了”,而是"这件事的交互密度是不是被低估了"。

一句话记住

工作量随规模按 1.5 次幂增长,生产率随交互密度与任务复杂度相差近十倍,而一半的工作周会被非技术事务吃掉——估算时必须把这三件事一起算进去。

19 - 第 9 章 · 削足适履

程序空间是用户成本的一部分,必须像硬件管元器件数量那样设规模预算;而真正能大幅省下空间的,是重新表达数据。

题记借西德尼·史密斯的话调侃作者:他"应该瞪大眼睛紧盯着诺亚,然后……好好学习一下,看他们当时是怎样把那么多东西塞进一个小小的方舟上的"。这一章讲的就是怎么把十磅东西塞进五磅的口袋。

这一章讲了什么

第一部分把"程序空间"还原成一笔钱。他用 IBM 的 APL 交互式系统算账:软件租金每月 400 美元,运行时至少占用 160K 字节内存,而在 Model 165 上内存租金约每千字节每月 12 美元。若程序全天可用,用户要付 400 美元的软件费加 1920 美元的内存租用费;每天只用 4 小时,则是 400 加 320 美元。内存不是免费的抽象资源,它按月计费。

由此他反驳了一句当年流传很广的抱怨——“2M 内存的机器,操作系统就占了 400K”。他说这种批评"就好像批评波音 747 飞机,仅仅因为它耗资 2700 万美元一样无知":首先要问的是"它能干什么",这笔内存租金换来的易用性和系统利用率,是否比投在其他硬件、编程人员或应用上更有效。当系统设计者判断常驻内存对用户比加法器、磁盘更有价值时,把硬件实现的一部分移到内存里是负责任的做法;反过来才是。所以他的立场是:不能一边提倡软硬件紧密集成,一边只拿规模本身去指责软件。

但这不是为臃肿开脱。规模既然是用户成本的大头,开发人员就必须像硬件工程师设定元器件数量目标那样,为软件设定规模目标、控制规模、寻找缩减办法。他的结论很克制:“规模本身不是坏事,但不必要的规模是不可取的。”

第二部分"规模控制"是 OS/360 的痛苦教训,三条都很具体。

第一条:只给核心程序设规模目标是不够的,必须把所有方面的规模编入预算。OS/360 是磁盘驻留系统,开发者对自由廉价的磁盘访问感到欣喜,却没有同时设定访问预算。后果是:当程序员发现自己的核心模块超标时,会把单元拆成覆盖模块——这个动作本身就增大了程序整体规模、降低了运行速度。更要命的是"我们的管理控制系统既没有度量,也没有捕获这些问题":每个人汇报的内核大小都在目标之内,所以没人发现规模出了事。直到性能模拟程序跑起来,暴露出 Fortran H 每分钟只能模拟编译 5 条语句、控制程序模块大量磁盘切换,问题才现形。他提炼出的道理是对称的——像制定驻留空间预算一样制定总体规模预算,像制定规模预算一样制定后台存储访问的预算。

第二条:在指明模块有多大的同时,要确切定义模块的功能。因为他们的做法是先分配空间预算、后分配功能,于是任何规模超标的程序员都会检查"哪部分能扔给别人"——结果是控制程序管理的缓冲区变成了用户空间的一部分,而所有控制模块都这么做,彻底影响了系统的稳定与安全性。

第三条更深刻,也更像人话:项目规模太大、缺乏管理和沟通,以至于每个团队成员都以为自己是"争取小红花的学生",而不是构建系统软件产品的人员。为了满足各自的目标,人人都在局部优化自己的程序,很少有人停下来考虑对客户的整体影响。作者断言,对大型项目来说,这种导向加上缺乏沟通是最大的危险。对策有两层:系统结构师必须在整个实现过程中保持持续警觉,确保连贯的系统完整性;而培养开发人员"从系统整体出发、面向用户"的态度,是软件编程管理人员最重要的职能。

第三部分"空间技能"是方法层面。既然预算和控制本身不会让程序变小,就需要创造性和技能。他给了三个抓手:

  • 用功能交换尺寸:为程序保留多少可选项目,是个策略问题。要么提供很多各自占少量空间的功能,要么设计成若干选项分组、按组裁剪程序——任何一组特殊选项被合并成整体计价时,需要的空间都更少。他用汽车打比方:把照明灯、点烟器和时钟作为整套配件标价,比单独提供便宜。所以设计者必须决定用户可选项目的粗细程度。
  • 警惕小模块的隐性代价:当内存被限制在一定范围内时,即使最细密的功能模块,其适用范围也难以推广。在最小规模的系统里,大部分模块被覆盖,相当一部分常驻空间要拿去做交换页面或暂存区,于是那个尺寸决定了所有模块的尺寸。把功能分解成很小的模块本身就要耗费空间、降低性能;等大型系统(有 20 倍暂存空间)来复用这些模块时,省下的只是访问次数,模块过小造成的空间与速度损失依旧存在——这实际上限制了"用小系统的模块搭出高效大系统"的可能。
  • 空间—时间折中:对给定功能,空间越多速度越快,这条在很大范围内成立——正是它让空间预算成为可能。

此外他补充了两条组织性的做法:确保编程技能得到培训而不是只依赖个人天赋与经验(换新语言或新机器时尤其重要);以及开发公共库——队列、搜索、散列、排序的例程或宏库,每项功能至少准备两个实现,一个跑得快、一个短小精炼,而这项工作可以与系统设计并行。

最后一部分"数据的表现形式是编程的根本"是全书最有洞察力的段落之一。作者说,精湛的技艺出自创造,而技艺改进的结果往往是战略突破,而不只是技巧提高。这种突破有时来自新算法(他举 Cooley-Tukey 快速傅立叶变换,或把比较算法复杂度从 n² 降到 n log n),但更普遍的是来自数据或表的重新表达:

如果提供了程序流程图,而没有表数据,我仍然会很迷惑。而给我看表数据,往往就不再需要流程图,程序结构是非常清晰的。

他举了两个例子:一位年轻人负责 IBM 650 的精密控制台解释器,发现用户交互频率低、速度慢,但空间极其昂贵,于是写了一个解释器的解释器,最终把程序空间压缩到不可思议的程度;Digitek 那个小而优雅的 Fortran 编译器用极为密集、专业化的代码表达自己,以至于不再需要外部存储——解码要损失时间,却因为避免了输入输出反而得到 10 倍补偿。他的结论是:被空间逼到墙角的程序员,常常能通过从代码里挣脱出来、回头审视程序的数据,得到极好的结果。

我的判断

  • 这一章表面上讲内存,实质讲的是把一个被当成"技术内部问题"的东西还原成用户成本。这与第 4 章的"易用性优先"是同一条思路:凡是用户最终要付钱或付出代价的东西,就必须进入设计目标,而不是留给实现者凭品味裁量。
  • “波音 747"那段反驳我认为今天依然稀缺。资源占用高本身不是罪,问题是它换来了什么。反过来,一个占用极小但不好用的系统同样是失败的设计——作者没有站在任何一边,而是要求算总账。
  • 三条 OS/360 教训里,第一条最技术、第三条最人性,但第二条我读着最心惊:先给空间、后给功能,会诱导每个人把复杂度推给邻居。缓冲区从控制程序挪进用户空间,单看每个模块都合规,合起来就危及稳定与安全。这是典型的"局部合规、全局失守”,任何指标体系都可能诱发它。
  • “每个人都以为自己是争取小红花的学生"这句话,是我在本书里见过最生动的组织诊断。它解释了为什么一群聪明人会把系统做坏——不是能力问题,而是激励方向与产品方向不一致。
  • 我对"空间—时间折中在很大范围内成立"这条持保留:在今天的分布式与缓存场景里,空间换时间的适用区间被架构局部性扭曲得很厉害,多占内存未必更快。但它作为预算机制(给每个功能一个空间额度,逼你在设计期做取舍)依然有效,这与第 5 章末尾"给每个功能分配 m 字节、n 微秒"是同一个手法。
  • “数据的表现形式是编程的根本"这句我完全认同,并且认为它是全章的落点:前文的预算、控制和技能都是防守手段,只有重新表达数据才是真正的进攻手段。程序的复杂度往往不在代码里,而在数据结构是否选对。

和我手上的工作有什么关系

  • 我写的是嵌入式与 MCU 上的代码,这一章的账算得格外直白:RAM 和 Flash 就是钱。以前我按"能不能塞进去"判断实现,现在会多问一句每千字节换来了什么功能、这个功能用户是否真的用得到——这正好对应作者说的"用功能交换尺寸”。
  • 三条 OS/360 教训里,第三条几乎逐字命中我的工作方式:一个人做项目时,很容易退化成"把每个模块写好看"的局部优化,而系统整体的一致性和用户的真实感受没人负责。作者给出的对策——结构师保持持续警觉 + 培养面向整体的态度——在单人项目里就是要求我阶段性地从代码里退出来,检查系统而不是模块。
  • 公共库那一条我打算补上:给这个站点和手上的嵌入式项目分别建一个可复用的函数与片段库(队列、环形缓冲、排序、状态机模板),每个功能至少两种实现——一个求速度、一个求尺寸。作者说这项工作可以与系统设计并行,恰好适合交给 Agent 批量完成。
  • “数据的表现形式是编程的根本"是我这章收获最大的一句。回看自己写过的驱动代码,最难的 bug 几乎都出在数据结构选错(缓冲区与索引的所有权、状态的存放位置)而不是算法写错。以后遇到复杂度异常的地方,先审数据,不先审逻辑。
  • 空间预算这个动作也值得移植到内容工作上:这个站点每加一个栏目、一门语言、一个主题,都有维护成本。给站点也设一个"规模预算”,比事后清理要省力得多。

一句话记住

规模不是罪,不必要的规模才是;要像硬件工程师管零件那样为软件设规模与访问预算,但真正把程序变小、变清楚的,是重新表达它的数据。

20 - 结束语 · 令人向往、激动人心和充满乐趣的 50 年

作者用一段个人回忆收尾:从 13 岁读到 Mark I 的报道,到看着四代计算机革命过去——以及我从这本书带走的论断清单。

写完十九章之后,这一节只有两页半。作者没有再讲方法论,而是回到了自己 13 岁那年的一个下午。

这一节讲了什么

他记得 1944 年 8 月 7 日读到哈佛 Mark I 型计算机研制成功的报道时那种"向往和开心"的感觉——那时他十三岁。他顺手把当时的角色分清了:Mark I 是电子机械学上的奇迹,Aiken 是它的结构设计师,IBM 的工程师 Clair Lake、Benjamin Durfee 和 Francis Hamilton 是它的实现设计师。这个区分放在全书的末尾不是偶然:体系结构与实现分离,是这本书从头讲到尾的一件事。

另一件让他同样向往的事,是读到 Vannevar Bush 在 1945 年 4 月发表的《诚如所思》(As We May Think)——那篇文章建议把大量知识组织成超文本式的网络,让人可以顺着链接漫游。他把这条线索与自己在第 19 章讲的"个人计算机带来了全新的创造性活动介质"接了起来。

1952 年,他在 IBM 纽约恩迪科特拿到一份暑期工作,第一次在 IBM 604 上真正编程,也了解了 IBM 701(第一台存储程序计算机)的正式指令集;从哈佛的 Aiken 和 Iverson 名下毕业之后,职业梦想变成了现实。他在这里写下了一句很难不被打动的话:"感谢上帝,让我成为为数不多的那些开开心心地做着自己喜欢的工作的人之一。"

接着是一段技术史的白描:从真空管到晶体管再到集成电路;他用来工作的第一台计算机是 IBM 7030 Stretch——1961 到 1964 年世界上运算最快的机器,一共卖出九台;而他写这一章时用的 Macintosh PowerBook,“不但快,还有大容量内存和大容量硬盘,而且便宜了 1000 倍(如果按定值美元来算,便宜了 5000 倍)"。他依次经历了计算机革命、电子计算机革命、小型计算机革命和微型计算机革命——每一次都带来计算机数量的剧增。

最后一段讲的是知识本身。五十年代中期他刚毕业时,能看完当时所有的期刊和会议报告,掌握所有潮流动向;而写这段话时,他只能对层出不穷的学科分支说"再见”,对自己关注的东西越来越难以全部掌握。他的结论是全书最轻的一句话,也是最重的一句:

兴趣太多,令人兴奋的学习、研究和思考的机会也太多——多么不可思议的矛盾啊!这个神奇的时代远远没有结束,它依然在飞速发展。更多的乐趣,尽在将来。

我从这本书带走什么

第 18 章里作者把 1975 年版的论断逐条抽出来打分,并说了一句我印象很深的话:清晰和错误都比模糊不清好得多。系列结束,我也按同样的格式给自己留一份清单——标明哪些我确认,哪些我存疑。

我确认的

论断理由
人月不可互换:沟通成本随人数按 n(n-1)/2 增长,加人往往延长进度算术上成立,与我的协作经验一致
概念完整性是产品质量的核心,它只能来自少数人的一致决策我自己每次返工,几乎都能追到"结构性分歧"而非"功能缺失"
第一个系统多半不合用,计划里应当包含丢弃与增量开发、快速原型的实践互相印证
缺陷修复会以相当高的概率引入新缺陷,回归测试不能省与我的调试经验完全吻合
里程碑必须具体到无法自欺模糊的里程碑会让我对自己撒谎,这是最廉价的自欺
工作的非技术时间占比极高(当年数据是一半)与我的实际感受一致,且我此前从未把它计入预算
文档应当并进源程序,信息的唯一来源才是不漂移的来源这个站点的做法已经验证
人的素质与组织,比工具与方法更决定成败工具提高下限,人决定上限
真正的进展来自逐步积累,不会来自单点突破十年、二十年过去,作者的预言没有失效

我存疑的

存疑点我的保留
复杂度是纯粹的根本困难,任何时代用任何方法都存在部分复杂度其实是组织失误的投影,因此可被消除,作者自己也承认这一点
第二个系统必然最危险在高频迭代的环境里,“第二系统效应"可能被稀释,也可能以每次重写的形式反复出现
图形化表达对软件设计价值有限数据流、状态机、时序图在工程沟通中确实有效,这是作者持续的盲区
语音会成为表达动词的方法几十年过去并未发生,说明交互范式的预测远比工程管理难
程序验证不必苛求今天的形式化方法与类型系统在关键系统里承担了实实在在的工作
十五条规则式的清单可以照搬我更需要其中的判断标准,而不是清单本身

这个系列本身

按最初定下的读法,这本书是逐章读完的:先做一遍检视阅读,再一章一篇地写,每篇都先复述作者的论证,再下自己的判断,最后接到手上的工作上。二十篇写完,我最大的收获反而不是任何单条结论,而是作者示范的一件事:他能在二十年之后回头给自己逐条打分,并且明确写出"Parnas 是正确的,我是错误的”。

这本书出版时讲的是 1960 年代的机器和项目,但它讨论的问题——概念由谁决定、沟通成本如何增长、变更如何被引入、质量由谁背书——没有一条随着技术进步而消失。倒是那些依赖当时资源约束的判断(内存多贵、机时多稀缺、语言有多少选择),几乎全都过期了。这个对比本身就是一条判据:判断一个技术结论值不值得记住,就看它依赖的是人的特性,还是当时的资源条件。

至于"没有银弹"这个结论,我读完这二十篇之后的理解比之前更具体了:它不是让人放弃改进,而是让人放弃对单点突破的期待,把精力放回可控的逐步积累上。作者在最后一段说的"更多的乐趣,尽在将来",我认为正是这个意思——不是等技术来救,而是承认这项工作本身就是这样,然后继续做下去。

21 - 我为什么用 OINK 搭这个站

选型、取舍与放弃的方案:静态站、Markdown 单一内容源、人和 Agent 共用一份材料。

这个站点是用来放技术笔记、项目记录与决策备忘的。开工第一件事不是选模板,而是想清楚它要承担什么:既要给读者看,也要能被 Agent 直接读取,还要在几年后仍然能改。

结论先给

用 Hugo + OINK 主题,纯静态构建、Markdown 单一内容源、Cloudflare Pages 托管。

理由有三条。

一、内容源只有一份

OINK 的每一页都同时产出 HTML、Markdown、打印视图与 llms.txt 索引。人和 Agent 读到的是同一份源,不需要维护两套材料。

hugo.yaml 中的输出格式
outputs:
  home: [HTML, RSS, markdown, LLMS]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]

二、正文就是 Markdown

提示块、标签页、步骤、参数表、文件树、图表都用原生 Markdown 语法加一行属性表达,源码本身仍然可读,不引入 MDX 或组件语法。

说明

写页面时不要引入只在渲染后才有意义的写法;下一个接手的人(或 Agent)读的是源文件。

三、构建链足够短

一个 hugo 二进制加 Go 模块解析,没有 Node、npm、打包器,也没有 CDN 依赖。本地预览是毫秒级热重载,发布交给 CI。

放弃的方案

方案放弃原因
现成的博客 SaaS内容不在自己手里,导出与迁移都受制于人
前端框架 + 静态导出为了写文章维护一套 npm 依赖链,收益不匹配
手写 HTML组件与多语言要全部自己来,长期维护成本高

选型是决策,不是事实。写在这里,是为了将来想换的时候知道自己当初为什么这么选。