《Google 软件工程》:如何写好文档?
重点总结
文档核心理念与重要性
技术文档是工程组织成功的关键,但其价值往往被低估。核心理念是将文档视作代码,并将其融入工程师现有的工作流程中,从而降低编写门槛,提升文档的质量与生命力。
核心问题 | 解决方案/观点 |
---|---|
为什么重要? | • 代码和API更易于理解,减少错误。 • 帮助团队明确和专注目标。 • 简化流程,降低新成员上手难度。 • 一次编写,千百次阅读,长期回报巨大,其价值会在所有读者中摊销。 |
为何工程师不爱写? | • 收益延迟:好处不直接体现在作者身上。 • 技能误区:认为写作是独立于编程的技能。 • 工具缺乏:没有很好地集成到开发工作流程中。 • 额外负担:被看作是需要维护的又一样东西。 |
对作者的好处 | • 打磨API:写不出清晰的文档,说明API设计得不够好。 • 维护路线图:为未来的自己提供历史记录和背景。 • 专业形象:良好的文档通常意味着项目维护得更好。 • 减少被打扰:一次性写好,胜过反复向他人解释。 |
核心方法:像对待代码一样对待文档
将文档工作流程与软件开发流程对齐,是提升文档质量和工程师参与度的关键。
- 明确所有权:没有所有者的文档会迅速过时。明确的责任人可以处理问题和更新。
- 纳入版本控制:将文档与它所描述的代码一起存放在源代码控制系统中。
- 进行审查:文档的修改应和代码一样被审查(Code Review),确保其准确性和清晰度。
- 追踪问题:像追踪代码的Bug一样,为文档建立问题追踪和反馈机制。
- 建立规范:创建唯一的、权威的“规范文档”(Canonical Document),避免信息重复和冲突。在谷歌,
go/links
快捷链接工具对此帮助巨大。 - 案例研究:GooWiki的演变:谷歌早期的维基(GooWiki)因缺乏所有权和流程而失败,文档混乱且过时。将其迁移到源代码控制中,并与开发工作流程绑定后,文档质量显著提升。
了解你的受众
编写文档时最常见的错误是只为自己写。在动笔前,必须明确你的读者是谁。
受众类型 | 特点 | 写作关键点 |
---|---|---|
寻求者 (Seeker) | 知道自己想要什么,来查找特定信息。 | 一致性 (Consistency)。使用标准化的格式和动词(如函数注释以动词开头),方便快速扫描和定位。 |
浏览者 (Browser) | 不确定自己想要什么,可能只有一个模糊的概念。 | 清晰性 (Clarity)。提供概述、介绍和“TL;DR”(太长不看)摘要,快速解释代码或文档的用途。 |
关键实践:
- 平衡专家与新手:保持文档简短清晰。先写下所有内容,然后编辑提炼,删掉冗余信息。
- 分离客户与内部:将面向API用户的文档(客户)与面向团队内部的实现细节、设计决策文档(供应方)分开。
文档的类型与职责
每份文档都应有单一的职责。不要试图在一个文档里做所有事,这会导致混乱和不可读。
文档类型 | 主要职责与目的 | 关键编写技巧 |
---|---|---|
参考文档 (Reference) | 描述“如何使用”。最常见的类型,如代码注释。 | • 源于代码:直接写在代码中(如头文件、Javadoc),并从中生成文档,保证单一来源。 • 文件注释:概述文件内容、主要用例和目标受众。 • 类注释:描述类的作用和重要方法,以名词为中心。 • 函数注释:描述函数的功能,以祈使动词开头(如 合并... 返回... )。自然地描述参数、返回和异常,避免死板的模板。 |
设计文档 (Design) | 描述“为什么”和“如何构建”。在重大项目开始前编写。 | • 协作编写,并由专家评审。 • 内容:包括设计目标、替代方案及其权衡、实施策略、安全/隐私等影响。 • 作用:既是开发蓝图,也是衡量项目成功的历史记录。 |
教程 (Tutorial) | 手把手引导完成特定任务,如“Hello World”。 | • 无前置假设:假定读者是新手。 • 明确编号:只对用户需要执行的每一步操作进行编号。 • 清晰展示:将用户输入和系统输出用代码块等形式清晰标出。 |
概念文档 (Conceptual) | 提供高层概述和解释,补充参考文档。 | • 牺牲完整性换取清晰性:关注常见用法,忽略罕见边缘情况,以传达理解为首要目标。 • 文档的集成测试:解释多个API或模块如何协同工作。 |
着陆页 (Landing Page) | 扮演“交通警察”,为项目或团队提供入口。 | • 只做链接:清晰地标识页面用途,然后链接到其他具体的文档(如教程、设计文档等)。 • 分离内外:为团队内部和外部用户创建不同的着陆页。 |
文档的评审与维护
环节 | 目的 | 参与者 |
---|---|---|
技术评审 | 保证准确性 | 主题专家,团队成员(通常是代码审查的一部分) |
受众评审 | 确保清晰度 | 领域不熟悉的人,如新成员或API客户 |
写作评审 | 保证一致性 | 技术撰稿人或志愿者 |
文档的废弃与保鲜:
- 废弃文档:过时的文档是有害的。当文档不再有用时,应明确地将其删除或标记为过时。
- 保鲜日期 (Freshness Date):在文档中加入元数据,记录其最后审查日期和所有者。系统可以定期提醒所有者进行审查,确保信息不过时。
原文翻译
在大多数工程师对编写、使用和维护代码的抱怨中,一个常见问题是缺乏高质量的文档。“这个方法的副作用是什么?”、“我在第三步之后出错了”、“这个缩写词是什么意思?”、“这份文档是最新的吗?”——每个软件工程师在职业生涯中都曾对文档的质量、数量或完全缺失提出过抱怨,谷歌的软件工程师也不例外。
技术撰稿人和项目经理可以提供帮助,但软件工程师始终需要自己编写大部分文档。因此,工程师需要适当的工具和激励来高效完成这项工作。让工程师更轻松地编写高质量文档的关键在于引入能够随组织扩展并与现有工作流程相结合的流程和工具。
总体而言,2010年代末的工程文档状况与1980年代末的软件测试状态相似。每个人都认识到需要更多努力来改善它,但尚未在组织层面认识到其关键价值。这种情况正在改变,尽管速度缓慢。在谷歌,我们最成功的做法是将文档视为代码,并将其纳入传统工程工作流程,使工程师能够更轻松地编写和维护文档。
什么是合格的文档?
当我们提到"文档"时,指的是工程师为完成工作需要编写的所有补充文本:不仅包括独立文档,还包括代码注释(事实上,谷歌工程师编写的大部分文档都以代码注释形式存在)。本章将进一步讨论各类工程文档。
为什么需要文档?
高质量文档能为工程组织带来巨大收益:
- 代码和API更易理解,减少错误
- 当项目团队的设计目标和团队目标明确时,工作更专注
- 清晰列出的步骤使手动流程更易遵循
- 有完整记录的流程能大幅减少新成员加入团队或代码库的工作量
但由于文档的收益都具有滞后性,通常不会给作者带来直接好处。与测试不同(测试很快就能给程序员带来回报),文档编写通常需要更多前期工作,直到后期才会给作者带来明确收益。但就像测试投入一样,文档投入会随时间获得回报。毕竟,你可能只写一次文档,但会被阅读数百甚至数千次——其初始成本将在所有未来读者中分摊。
文档不仅能随时间扩展价值,对组织其他部分也至关重要。它有助于回答:
- 为什么做出这些设计决策?
- 为什么用这种方式实现这段代码?
- 如果你两年后回头看自己的代码,为什么要这样实现?
既然文档能传达这么多好处,为什么工程师普遍认为它"很糟糕"?如前所述,部分原因是这些好处不直接,尤其对作者而言。但还有其他原因:
- 工程师常认为写作是与编程分离的技能(我们将说明事实并非如此)
- 有些工程师觉得自己不擅长写作(其实不需要精通英语,只需从读者角度思考)
- 有限的工具支持或与开发者工作流程的集成使文档编写更困难
- 文档被视为额外负担(需要维护的其他东西),而非使现有代码维护更轻松的工具
不是每个工程团队都需要技术撰稿人(即使需要,也没有足够的技术撰稿人)。这意味着工程师基本上需要自己编写大部分文档。因此,我们不应强迫工程师成为技术撰稿人,而应考虑如何让工程师更轻松地编写文档。决定在文档上投入多少精力是组织需要做出的决策。
文档对多个群体都有益。即使对作者也有以下好处:
- 有助于完善API设计:编写文档是确定API是否合理的最可靠方法之一,常导致重新评估设计决策
- 提供维护路线图和历史记录:好的注释能帮助理解多年前编写的复杂代码
- 使代码更专业并带来流量:良好文档的API常被视为设计更好的API
- 减少其他用户提问:这可能是编写文档的最大长期收益
尽管这些好处对作者很显著,但文档的大部分收益自然累积到读者身上。谷歌《C++风格指南》提出"为读者优化"的格言,这不仅适用于代码,也适用于注释和API文档集。与测试一样,编写好文档的努力将在其生命周期内多次获得回报。随时间推移,文档变得极其重要,随着组织规模扩大,对特别重要的代码,文档将带来巨大收益。
把文档当做代码
使用单一主编程语言的软件工程师仍常使用不同语言解决特定问题(如用shell脚本或Python运行命令行任务,或用C++编写后端代码但在Java中编写中间件代码等)。每种语言都是工具箱中的工具。
文档也应如此:它是用不同语言(通常是英语)编写、用于完成特定任务的工具。编写文档与编写代码区别不大——它同样有规则、特定语法和样式规范,通常用于实现与代码类似的目的:加强一致性、提高清晰度和避免理解错误。在技术文档中,语法很重要不是因为需要规则,而是为了标准化表达,避免混淆或分散读者注意力。因此,谷歌对许多语言都有特定的注释风格要求。
与代码一样,文档也应有所有者。无主的文档会变得过时且难以维护。明确的所有权还能通过现有开发者工作流程(bug跟踪系统、代码审查工具等)更轻松地处理文档。当然,不同所有者的文档仍可能冲突。这时指定规范文档非常重要:确定主要来源,并将其他相关文档合并到该来源中(或弃用副本)。
在谷歌,“go/links”(见第三章)的普遍使用使这一过程更简单。有直接"go/links"的文件往往成为权威标准来源。另一种促进规范文档的方法是将它们直接置于源代码控制下,与所记录的代码直接关联。
文档通常与代码紧密相连,因此应尽可能将其视为代码。也就是说,你的文档应该:
- 有需要遵循的内部策略或规则
- 置于源代码控制下
- 有明确的所有权负责维护
- 修改需经审查(与所记录代码一起变更)
- 像追踪代码bug一样追踪问题
- 定期评估(某种程度上相当于测试)
- 如有可能,衡量准确性、时效性等指标(目前尚无相关工具)
工程师越将文档工作视为软件开发的必要任务,就越不会反感编写文档的前期成本,也越能获得长期收益。此外,简化文档工作可降低这些前期成本。
案例研究:谷歌维基
当谷歌规模较小时,几乎没有技术作家。分享信息最简单的方式是通过内部维基(GooWiki)。起初这看似合理——所有工程师共享可根据需要更新的文档集。
但随着谷歌规模扩大,维基方法的问题显现:
- 无真正文档所有者,许多文档变得过时
- 无添加新文档的流程,出现重复文档
- 扁平命名空间导致文档集缺乏层次结构
- 某些主题(如生产计算环境Borg的设置)有7-10个不同文档,大多未维护且团队特定
另一个问题是:能修复文档的人不是使用者。新用户要么无法确认文档是否有误,要么难以报告错误。他们知道有问题(因为文档不起作用),但无法"修复"。而最能修复文档的人通常不需要查阅自己编写的文档。随着谷歌发展,文档质量变得如此之差,以至于在年度开发者调查中成为首要抱怨。
改善方法是将重要文档转移到与代码相同的源代码控制下。文档开始有所有者、在源码树中的规范位置,以及识别和修复bug的流程——文档质量显著提升。此外,文档编写维护方式开始与代码相同:文档错误可在bug跟踪软件中报告,修改可通过现有代码审查流程处理。最终,工程师开始自行修改文档或发送修改给技术作家(通常是文档所有者)。
将文档移入源码控制最初引发很多争议。许多工程师认为取消GooWiki这一"信息自由堡垒"会导致质量下降,因为对文档的要求(需审查、需所有者等)更高。但事实相反——文档变得更好。
引入Markdown作为通用文档格式化语言也有帮助,它使工程师无需HTML或CSS专业知识就能轻松编辑文档。谷歌最终引入自己的框架g3doc用于在代码中嵌入文档。有了这个框架,文档进一步改善,因为在工程师开发环境中,文档与源代码并存。现在,工程师可在相同变更中更新代码及相关文档(我们仍在改进这一实践)。
关键区别在于:维护文档变得类似维护代码的流程——工程师提交bug、在变更列表中对文档修改、发送修改给专家审查等。利用现有开发者工作流程而非创建新流程是关键优势。
了解你的受众
工程师编写文档时最常见错误之一是只为自己写。这很自然,为自己写也有价值(你可能几年后需要回顾代码理解当初设计)。你也可能与读者具有相似技能。但若只为自己写,你会做出某些假设,而考虑到文档可能被非常广泛的读者(所有工程人员、外部开发者)阅读,即使失去几个读者也是很大代价。随着组织发展,文档中的错误更突出,你的假设常不适用。
相反,在写作前应(正式或非正式地)确定文档需要满足的受众。设计文档可能需要说服决策者;教程可能需要为完全不熟悉代码库的人提供非常明确的说明;API文档可能需要为该API的任何用户(无论专家或新手)提供完整准确的参考信息。始终尝试确定主要受众并为该受众写作。
我们已指出应根据受众技能水平和领域知识写作。但谁是你的受众?根据以下一个或多个标准,你可能拥有多个受众:
- 经验水平(专家级程序员,或甚至不熟悉语言的初级工程师)
- 领域知识(团队成员或仅熟悉API端点的其他工程师)
- 目的(需要API完成特定任务并快速找到信息的最终用户,或负责维护特别复杂实现的软件专家)
某些情况下,不同受众需要不同写作风格,但多数情况下,技巧是以尽可能广泛适用于不同受众群体的方式写作。通常你需要同时向专家和新手解释复杂主题。为有领域知识的专家写作可能让你少走弯路,但会让新手困惑;反之,向新手详细解释一切无疑会让专家厌烦。
这显然是种平衡行为,没有万能解决方案。但我们发现保持文档简短有帮助:写下足够描述向不熟悉主题的人解释复杂内容,但不失去或惹恼专家。编写简短文档通常需要先写较长版本(记录所有信息),然后编辑删除重复信息。这听起来可能乏味,但请记住:这项成本会分摊到所有读者身上。正如布莱斯·帕斯卡所说:"如果我有更多时间,我会写更短的信。"通过保持文档简短清晰,你将确保它能让专家和新手都满意。
另一个重要受众区分基于用户如何使用文档:
- 寻求者:知道想要什么,想确认看到的内容是否符合需求。关键教学手段是一致性。为这群体写参考文档(如代码文件内注释)时,希望注释遵循类似格式以便快速扫描引用。
- 浏览者:可能不确定具体想要什么。关键是清晰。提供概述或介绍(如文件顶部),解释查看代码的用途。确定文档何时不适合受众也很有用。谷歌许多文件以"TL;DR声明"开头,如"TL;DR:如果你对谷歌C++编译器不感兴趣,现在可以停止阅读。"
最后,重要区分是客户(如API用户)和供应方(如项目组成员)。为一方准备的文件应尽可能与另一方分开保存。实施细节对团队成员维护很重要,但最终用户不需要阅读。工程师常在他们发布库的参考API中表达设计决策,这种推理更适合放在设计文档中,或隐藏在接口后的实现细节里。
文档类型
工程师编写各种不同类型文档作为工作部分:设计文档、代码注释、操作文档、项目页面等。这些都算"文档",但重要的是了解不同类型,不要混合类型。通常,一个文档应有单一用途并坚持该职责。就像API应做好一件事,避免试图在一个文档中做多件事,而应更合理分解。
软件工程师常需编写的主要文档类型包括:
- 参考文档(含代码注释)
- 设计文档
- 教程
- 概念文档
- 着陆页
谷歌早期,团队拥有单页维基页面很常见,包含大量链接(许多已失效或过时)、系统工作原理的概念信息、API参考等混杂内容。这些文档失败因为它们没有单一职责(且变得太长没人阅读,某些著名wiki页面滚动几十屏)。相反,确保文档有单一职责,如果向页面添加内容无意义,可能需要找到或创建另一个用途文档。
参考文档
参考文档是工程师最常需要编写的类型;事实上,他们常需每天编写某种形式的参考文档。所谓参考文档,指的是记录代码库中代码使用情况的任何内容。代码注释是工程师必须维护的最常见参考文档形式,可分为两个基本阵营:API注释和实现注释。记住这两者的受众差异:
- API注释不需要讨论实现细节或设计决策,也不能假设用户像作者一样精通API
- 实现注释可假定读者有更多领域知识,但要小心假设过多:人员会变动,有时更安全的方法是有条理说明为何这样写代码
大多数参考文档(即使作为独立于代码的文档提供)也是由代码库本身注释生成的(这应该如此,参考文档应尽可能单一来源)。Java或Python等语言有特定注释框架(Javadoc、PyDoc、GoDoc)旨在简化参考文档生成。C++等语言没有标准"参考文档"实现,但由于C++将API表面(头文件或.h文件)与实现(.cc文件)分开,头文件通常是记录C++ API的自然位置。
谷歌采用这种方法:C++ API的参考文件应存在于头文件中。其他参考文档也直接嵌入Java、Python和Go源代码中。由于谷歌Code Search浏览器(见第17章)非常强大,我们发现提供单独的通用参考文档没什么好处。用户不仅能轻松搜索代码,通常还能找到代码原始定义作为最重要结果。将文档与代码定义放在一起也使文档更易被发现和维护。
我们都知道代码注释对良好文档化API必不可少。但什么是"好"注释?前文已确定参考文档两个主要受众:寻求者和浏览者。寻求者知道想要什么,浏览者不知道。对寻求者关键是注释代码库一致,可快速扫描API找到所需内容;对浏览者关键是明确识别API用途(通常在文件头顶部)。以下将介绍一些代码注释指南(适用于C++,但谷歌其他语言也有类似规则)。
文件注释
在谷歌,几乎所有代码文件都必须包含文件注释(仅包含一个实用函数的头文件等可能例外)。文件注释应以以下形式开头:
// -----------------------------------------------------------------------------
// str_cat.h
// -----------------------------------------------------------------------------
//
// 此头文件包含高效连接和追加字符串的函数:StrCat()和StrAppend()。
// 这些例程大部分工作实际上通过使用特殊AlphaNum类型处理,
// 该类型设计为参数类型,可高效管理到字符串的转换并在上述操作中避免拷贝。
... ...
通常,文件注释应以所包含代码内容概要开头,确定代码主要用例和目标受众(前例中是想连接字符串的开发者)。无法在一两段中简洁描述的API通常是未深思熟虑的标志,这时应考虑将API分成独立组件。
类注释
大多数现代编程语言都是面向对象的,因此类注释对定义代码库中使用的API"对象"非常重要。谷歌所有公共类(和结构)必须包含描述该类/结构、重要方法及目的的类注释。通常类注释应"名词化",强调其对象方面,如"Foo类包含x、y、z,允许你做Bar,并有以下Baz方面"等。
类注释通常应以以下形式开头:
// -----------------------------------------------------------------------------
// AlphaNum
// -----------------------------------------------------------------------------
//
// AlphaNum类作为StrCat()和StrAppend()的主要参数类型,
// 提供数字、布尔值和十六进制值(通过Hex类型)到字符串的高效转换。
函数注释
谷歌所有公开函数或类的公共方法也必须包含说明函数功能的函数注释。函数注释应强调其使用的主动性,以指示性动词开头描述函数作用和返回内容。
函数注释通常应以以下形式开头:
// StrCat()
//
// 合并给定字符串或数字,不使用分隔符,将合并结果作为字符串返回。
... ...
用声明性动词开始函数注释可在头文件中引入一致性。寻求者可快速扫描API,仅通过动词就知道函数是否合适。"合并、删除、创建"等。
某些文档样式(和一些文档生成器)要求在函数注释中加入"Returns:"、"Throws:"等模板,但在谷歌我们发现它们并非必须。在松散注释中呈现这类信息通常比分解到人为段落边界更清晰:
// 为具有给定名称和地址的客户创建新记录并返回记录ID,
// 如果具有该名称的记录已存在则抛出`DuplicateEntryError`。
int AddCustomer(string name, string address);
注意后置条件、参数、返回值和异常如何自然地记录在一起(本例中在一句话中),因为它们不是相互独立的。添加明确样板部分会使注释更冗长重复,但不会更清晰(甚至可能更不清晰)。
设计文档
谷歌大多数团队在开始重大项目前都需要批准的设计文档。软件工程师通常使用团队批准的特定设计文档模板编写拟定设计文件。这些文档为协作设计,通常在谷歌文档中共享(因其有良好协作工具)。某些团队要求在特定团队会议上讨论和辩论设计文件,专家可讨论或评论设计细节。某种程度上,这些设计讨论就像编写代码前的一种代码审查形式。
由于设计文档开发是工程师部署新系统前首先进行的过程之一,也是确保涵盖各种关切的机会。谷歌典型设计文档模板要求工程师考虑设计各方面,如安全影响、国际化、存储要求和隐私问题等。大多数情况下,设计文档的这些部分由相应领域专家审查。
好的设计文档应包括设计目标、实施策略,并提出关键设计决策(重点放在各自权衡上)。最佳设计文档会建议设计目标,涵盖替代设计并指出优缺点。
好的设计文档一旦批准,不仅可作为历史记录,还可衡量项目是否成功实现目标。大多数团队将设计文档归档在团队文档中适当位置供日后审查。产品发布前审查设计文档通常很有用,可确保编写时所述目标与发布时一致(如不一致,可相应调整文档或产品)。
教程
每个软件工程师加入新团队时都希望尽快上手。指导完成新项目设置的教程非常有价值;"Hello World"是确保所有团队成员从正确角度出发的最佳方式之一。这适用于文件和代码。大多数项目应有"Hello World"文档,不做任何假设并让工程师做些"真实"事情。
通常,如果没有教程,编写教程的最佳时机是首次加入团队时(这也是查找现有教程中bug的最佳时机)。用记事本等记下需要做的所有事情(无领域知识或特殊设置限制);完成后,你可能知道过程中犯了哪些错误及原因,然后可编辑步骤获得更精简教程。重要的是记下需要做的一切;尽量不要假设任何特定设置、权限或领域知识。如确实需要其他设置,请在教程开头明确说明这是先决条件。
大多数教程要求按顺序执行多个步骤。这时应明确为步骤编号。如教程重点为用户(如外部开发者文档),对用户需要执行的每个操作编号,不要对系统响应操作编号。执行此操作时,明确编号每个步骤至关重要——没什么比步骤4中忘记告诉某人授权用户名更令人恼火。
糟糕教程示例:
- 从服务器下载软件包
- 将shell脚本复制到主目录
- 执行shell脚本
- foobar系统将与认证系统通信
- 经过身份验证后,foobar将引导新数据库"baz"
- 通过执行SQL命令测试"baz"
- 类型:CREATE DATABASE my_foobar_db;
在前例中,步骤4-5发生在服务器端,不清楚用户需做什么(实际不需要),这些副作用应作为步骤3部分提及。同样不清楚步骤6-7是否不同(实际不是)。将所有原子用户操作组合到单一步骤,让用户知道每个步骤需要做什么。此外,如教程有用户可见输入/输出,用单独行表示(通常使用单间距粗体字体)。
改进后教程示例:
-
从服务器下载软件包:
$curl -I http://example.com
-
将shell脚本复制到主目录:
$cp foobar.sh ~
-
在主目录执行shell脚本:
$cd ~; foobar.sh
foobar系统将首先与身份验证系统通信。经过身份验证后,将引导新数据库"baz"并打开输入shell。
-
通过执行SQL命令测试"baz":
baz:$CREATE DATABASE my_foobar_db;
注意每个步骤都指定了用户操作。相反,如教程侧重其他方面(如"服务器生命周期"文档),应从该重点角度编号步骤(服务器的功能)。
概念文档
有些代码需要比参考文档更深入的解释或见解。这时我们需要概念文档提供API或系统概述。概念文档示例可能包括流行API库概述、描述服务器内数据生命周期的文档等。几乎所有情况下,概念文档都是为了补充而非取代参考文档集。这通常导致某些信息重复,但目的是提高清晰度。这时概念文档不必涵盖所有边缘情况(参考文档应严格涵盖),为清晰度牺牲一些准确性是可接受的。概念文档要点是传达理解。
"概念"文档是最难编写的文档形式,因此通常是软件工程师工具箱中最被忽视的类型。工程师编写概念文档时的一个问题是它通常无法直接嵌入源代码中(因为没有规范位置放置)。某些API具有相对广泛的API表面积,这时文件注释可能是对API进行"概念性"解释的合适位置。但API通常与其他API/模块协同工作,记录这种复杂行为的唯一合理方式是通过单独的概念文档。如果注释是文档的单元测试,概念文档就是集成测试。
即使API范围适当,提供单独的概念文档通常也有意义。例如Abseil的StrFormat库涵盖了API熟练用户应理解的各种概念。无论内部还是外部,我们都提供了格式概念文档。
概念文档需要对广大受众有用(包括专家和新手),还需强调清晰性,因此通常需要牺牲完整性(最好留给参考文档)和(有时)严格准确性。这不是说概念文档应故意不准确,而是应关注常见用法,将罕见用法或副作用留给参考文档。
着陆页
大多数工程师都是团队成员,而大多数团队在公司内部网某处有"团队页面"。通常这些网站有些混乱:典型着陆页可能包含一些有趣链接,几个标为"先读此文!"的文件,以及一些既为团队又为客户的信息。这些文档起初有用,但很快变成灾难——因为维护变得非常麻烦,最终会非常过时,只有勇敢或绝望的人才会修复。
幸运的是,这些文档看似吓人,实际很容易修复:确保着陆页明确标识其用途,然后只包含指向其他页面的链接获取更多信息。如果着陆页上某件事不只是做交通警察,它就没做好本职工作。如有单独设置文档,从着陆页作为单独文档链接。如着陆页有太多链接(页面不应滚动多屏),考虑按分类法将页面分成不同部分。
大多数配置不当的着陆页有两个不同用途:产品或API用户的"入门"页面,或团队主页。不要让页面同时服务两个主体——这会变得混乱。创建独立"团队页面"作为主着陆页之外的内部页面。团队内部需要知道的内容通常与API客户需要知道的完全不同。
文档评审
在谷歌,所有代码都需要评审,代码评审已被充分理解和接受。通常文档也需要评审(尽管接受度不如代码评审普遍)。如果想"测试"文档是否有效,通常应让他人评审。
技术文档受益于三种不同类型的评审,每种关注不同方面:
- 技术评审:保证准确性。通常由主题专家(常是团队成员)完成,常是代码审查本身的一部分。
- 受众评审:确保清晰度。通常由不熟悉该领域的人(如新团队成员或API客户)完成。
- 写作评审:保证一致性。通常由技术撰稿人或志愿者完成。
当然这些界限有时模糊,但如果文档引人注目或可能外部发布,可能希望确保它收到更多类型评审(本书采用了类似评审程序)。任何文档都倾向于从上述评审中受益,即使某些评审是临时性的。也就是说,即使让一个审查员评审文本也比没人评审好。
重要的是,如果文档与工程工作流程相连,它往往会随时间改进。现在谷歌大多数文档都隐式经过受众审查,因为在某个时刻,读者会使用这些文档并希望在不起作用时(通过bug或其他反馈形式)让你知道。
案例研究:开发者指南库
如前所述,大多数(几乎所有)工程文件包含在共享维基中存在一些问题:重要文档无主、重复文档、过时信息,以及难以归档的错误或文件问题。但某些文档不存在这些问题:谷歌C++风格指南由一组精选高级工程师(风格仲裁者)管理。该文档保持良好状态因为有人关心它——他们隐式拥有该文档。该文档也是规范的:只有一个C++风格指南。
如前所述,直接位于源代码中的文档是促进规范文档建立的方法之一;如果文档与源代码放在一起,它通常应是最适用的(希望如此)。在谷歌,每个API通常有单独g3doc目录存放这些文档(写为标记文件,在代码搜索浏览器中可读)。将文档与源代码放在一起不仅建立事实上的所有权,还使文档看起来更完全是代码的"一部分"。
然而,某些文档集在源代码中没有明确位置。例如谷歌"C++开发者指南"在源代码中没有明确主"C++“目录供人查找这些信息。这时(及其他跨API边界情况),在自己的仓库中创建独立文档集变得非常有用。许多这类文档将关联现有文档挑选到公共集合中,具有公共导航和外观。这些文档称为"开发人员指南”,与代码库中的代码一样,在按主题而非API组织的特定文档库中受源代码控制。通常技术撰稿人管理这些指南,因为他们更擅长解释跨API边界的主题。
随时间推移,这些开发者指南成为经典。编写重叠或补充文档的用户在规范文档集建立后,开始愿意将文档添加到规范集中,然后废除重复文档。最终,C++风格指南成为更大"C++开发者指南"的一部分。随着文档集变得更全面权威,质量也提高。工程师开始记录错误因为他们知道有人在维护这些文档。由于这些文档锁定在源码控制下并有适当所有者,工程师也开始直接向技术作者发送变更列表。
引入go/links(见第3章)后,大多数文件实际上能更容易建立自己在特定主题上的规范性。例如我们的《C++开发指南》建立在"go/cpp"上。有了更好的内部搜索、go/links,以及将多文档整合到共同文档集,这样的规范文档集随时间变得更权威强大。
写文档秘诀
注:以下部分更像关于技术写作最佳实践的论文(和个人观点),而非"谷歌如何做"。对软件工程师来说,可考虑完全掌握,尽管理解这些概念可能让你更容易写出技术信息。
谁,什么,何时,何地,为什么
大多数技术文档回答"如何"的问题:如何工作?如何对此API编程?如何设置服务器?因此软件工程师有倾向在任何文件中直接跳到"如何",忽略相关问题:谁、什么、何时、何地和为什么。诚然这些问题通常不如"如何"重要(设计文件例外,其相当部分常是"为什么"),但若无适当技术文档框架,文档最终会混乱。尝试在任何文档前两段解决其他问题:
- WHO:受众。有时需在文件中明确说明受众,如"本文档适用于秘密向导项目的新工程师。"
- WHAT:确定文档用途,如"本文档是旨在测试环境中启动Frobber服务器的教程。"有时只需编写帮助你正确构建文档的内容。如开始添加不适用于WHAT的信息,可能需要将该信息移到单独文档。
- WHEN:确定文档创建、审查或更新时间。源代码中的文档隐式记录该日期,某些发布方案也自动记录。如没有,确保在文档上注明编写日期(或最后修订日期)。
- WHERE:通常也隐含,但要决定文档应放在哪里。通常偏好某种版本控制下,最好与所记录源代码一起。其他格式也适用于不同目的。在谷歌,我们常用Google Docs方便协作(特别在设计问题上)。但某些时候,任何共享文件不再像讨论而更像稳定历史记录,这时应移到更永久位置,有明确所有权、版本控制和责任。
- WHY:设定文档目的。总结希望别人阅读后能从文档中获得什么。好经验法是在文件引言中确立"为什么",写总结时验证是否达到最初期望(并相应修改)。
开头、中间和结尾
所有文档(实际文档的所有部分)都有开头、中间和结尾。虽然这听起来简单,但大多数文档通常至少应有这三部分。只有一个部分的文档只有一句话要说,很少文档只有一句话要说。不要害怕在文档中添加条款——它们将流程分解为逻辑部分,并为读者提供文档内容路线图。
即使最简单的文档通常也不止一句话要说。我们受欢迎的"每周C++提示"传统上非常简短,集中在一个小建议上。但即使在这里,有些章节也有帮助。传统上第一部分表示问题,中间是推荐解决方案,结论总结要点。如文档只有一个部分,某些读者无疑会难以找出重要要点。
大多数工程师厌恶冗余,这有道理。但在文档中,冗余通常有用。隐藏在文字墙内的要点可能难记住或梳理;而前面突出放置该点可能丢失后面提供的背景。解决方法是在介绍性段落中介绍和总结要点,然后用本节其余部分更详细阐述。这时冗余有助于读者理解所述内容重要性。
良好文档的衡量标准
好文档通常有三方面:完整性、准确性和清晰性。你很少在同一文档中得到这三点——例如试图使文档更"完整"时,清晰度可能开始受影响。如试图记录API每个可能用例,最终可能得到难以理解的混乱。对编程语言,在所有情况下完全准确(及记录所有可能副作用)也会影响清晰度。对其他文档,试图弄清复杂主题可能微妙影响文档准确性;你可能决定忽略概念文档中某些罕见副作用,因为其目的是让人熟悉API使用而非提供所有预期行为的教条式概述。
每种情况下,"良好文档"定义为有效文档。因此通常不希望文档承担多于一个任务或职责。对每个文档(及每种文档类型),确定其重点并适当调整写作。写概念文档?可能不需涵盖API每个部分。写参考文档?可能希望完整但必须牺牲一些清晰度。写着陆页?专注组织并最小化讨论。所有这些都是为提高质量(诚然很难准确衡量)。
如何快速提高文档质量?关注受众需求。通常少即是多。例如工程师常犯的错误是将设计决策或实现细节添加到API文档中。就像应在设计良好的API中将接口与实现分离一样,应避免在API文档中讨论设计决策——用户不需要知道这些信息。相反,将这些决策放在专门文档中(通常是设计文档)。
废弃文档
就像旧代码可能导致问题一样,旧文档也可能导致问题。随时间推移,文档会变得陈旧、过时或被废弃。尽可能避免使用过时文档,但当文档不再有用时,请删除或标识为已过时(如可能,指明获取新信息的位置)。即使对于无主文档,有人加上"这不再有效!"注释也比什么都不说(留下看似权威但无效的内容)更有帮助。
在谷歌,我们常在文档中附加"保鲜日期"。这类文档记录最后一次审阅时间,文档集元数据会在文档未触及时(如三个月)发送电子邮件提醒。如下示例的这些更新日期及作为bug跟踪文档有助于使文档集随时间更易维护(这是文档主要问题):
<!--*
# 文档新鲜度:更多信息见go/fresh-source。
freshness: { owner: `username` reviewed: '2019-02-27' }
*-->
拥有此类文档的用户有保持新鲜度的动力(如文档受源代码控制,需代码审查)。因此这是确保文档不时被查看的低成本方法。在谷歌,我们发现这种新鲜感中包括文档所有者及"Last Review by…"日期也增加了采用率。
何时需要技术撰稿人?
谷歌早期和发展时,软件工程中没有足够技术撰稿人(现在仍如此)。被认为重要的项目往往会得到技术撰稿人,无论团队是否真的需要。想法是技术撰稿人可减轻团队编写维护文档的一些负担,(理论上)让重要项目发展更快。这被证明是错误的假设。
我们了解到,大多数工程团队可完美为自己(团队)编写文档;只有为另一个受众编写文档时才倾向于需要帮助(因为这很困难)。团队内部关于文档的反馈回路更直接,领域知识和假设更清晰,感知需求也更明显。当然技术撰稿人通常在语法和组织方面做得更好,但支持团队不是对有限专业资源的最佳利用——它没有规模化。它引入了不正当激励:成为重要项目,你的软件工程师就不需写文档了。不鼓励工程师编写文档,结果与预期恰恰相反。
由于是有限资源,技术撰稿人通常应关注软件工程师作为正常职责部分需要完成的任务。这常涉及编写跨API边界的文档。项目Foo可能清楚知道需要什么文档,但可能不太清楚项目Bar需要什么。技术撰稿人更能以不熟悉该领域的人的身份出现。事实上这是他们的关键角色之一:挑战团队对项目效用的假设。这就是为什么许多(如非大多数)软件工程技术撰稿人倾向于关注这类特定API文档的原因之一。
总结
过去十年中,谷歌在解决文档质量方面取得长足进步,但坦率地说,谷歌文档还不是一等公民。相比之下,工程师已逐渐接受测试对任何代码修改都是必要的(无论多小)。同样,测试工具健壮、多样,并在不同点插入工程工作流程中。文档还未达到相同层次的扎根程度。
公平地说,解决文档问题的必要性不一定和测试相同。测试可以是原子化的(单元测试),可遵循规定的形式和功能。大多数情况下文档做不到。测试可自动化,而文档自动化方案通常缺乏。文档必然是主观的;其质量评价取决于读者而非作者,且常是异步的。尽管如此,人们认识到文档重要性,围绕文档开发的过程也在不断改进。笔者认为谷歌文档质量比大多数软件工程公司要好。
为改变工程文档质量,工程师和整个工程组织需要接受他们既是问题又是解决方案。他们需要意识到制作高质量文档是他们工作的一部分,从长远看这能节省他们的时间和精力,而非在当前文档状态下束手无策。对任何生命周期超过几个月的代码,记录该代码的额外周期不仅有助于他人,也有助于维护该代码。
内容提要
- 随时间推移和规模增长,文档变得极其重要
- 文档更新应利用现有开发人员工作流程
- 让文档集中在一个职责(用途)上
- 为你的受众而非自己写作