# 第 15 章 · 另外一面

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

---

LLMS 索引： [llms.txt](/llms.txt)

---

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

## 这一章讲了什么 {#summary}

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

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

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

### 需要什么样的文档

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

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

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

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

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

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

### 流程图

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

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

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

### 自文档化的程序

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

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

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

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

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

## 我的判断 {#reading}

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

## 和我手上的工作有什么关系 {#relevance}

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

## 一句话记住 {#takeaway}

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