跳转到主要内容

第 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 的故事对我有一个具体提醒:如果想要协作对象遵循某种规范,把规范变成可执行的示例比写更多的说明有效——一个模板文件、一条构建命令,胜过一页约定。

一句话记住

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