针对库开发者的创建信息丰富的文档的最佳实践
你为你的库提供的文档是非常重要的. 它可以决定使用者是否深入了解你的库, 是否在他们的项目中采用它, 在遇到困难时是否会继续坚持下去. 现在的开发者在各种语言, 库, 框架和平台之间, 拥有前所未有的选择. 因此, 吸引你的使用者, 并为他们提供各种信息, 是非常重要的; 否则, 他们可能会寻求其他的选择.
在你的库的最早的版本中, 来自使用者的反馈会非常稀少. 幸运的事, 创建并完善文档可以形成一个反馈循环, 大大提升你的项目的质量. 因此, 创建文档不应该看作一种负担, 也不应该在创建库时放在优先事项中较低的位置.
有效的文档不仅可以为使用者提供信息, 而且还能推动你的库的开发和完善. 下面是文档指导你的开发过程的几种主要方式:
你应该能够通过几段文字解释, 你的库做什么, 谁能够从中受益, 以及与其它方案相比有什么优势. 如果你做不到, 那么请重新考虑你的项目的范围和目标.
你应该能够创建一个 "入门" 指南, 让潜在的使用者能够尽量快速的启动. 怎么样才叫做 快速, 取决于问题领域, 但你可以与其他平台上类似的库进行比较. 这个指南应该引导使用者进入反馈循环, 这个循环会变得更加简单, 更加快速, 而且始终产生可靠的结果. 创建这个指南能够帮助你识别出那些阻碍使用者进步的突然增加的复杂性(悬崖边缘).
为函数编写文档的过程会迫使你考虑所有的边缘情况, 例如输入的有效范围, 可能抛出的异常, 以及随着负载的增加性能会如何下降. 这个过程经常会导致函数签名的改进, 以及底层实现的改进.
如果初始化你的库所需要的代码总是超过完成一个任务所需要的代码, 请重新考虑你的配置选项.
如果你不能创建清晰的示例, 使用标准选项执行基本任务, 请考虑为日常的使用优化你的 API.
如果你不能演示如何在不使用真实的数据源和在线服务的情况下测试你的库, 请考虑为访问网络(一般来说, 访问外部世界)的组件提供测试替身.
你越早为你的库提供文档 , 它就能够越早供真实世界的使用者测试. 然后就能够利用来自这些测试的反馈改进库的设计.
提供全面的文档
你的库必须提供充足的文档, 让使用者能够以最小的负担来使用它. 这个文档应该包括:
入门指南
的深入描述
针对常见使用场景的更长的示例 (也称做菜谱)
各种资源的链接, 例如 Blog, 文章, 在线研讨会, 会议演讲等等
入门指南应该包括你的库如何与它支持的构建系统集成. 应该包括对最常使用的实体的简要描述, 以及如何使用这些实体的小示例. 在库与外部世界交互的每个地方, 要指定配置环境的必要步骤, 以及如何验证是否成功完成了这些步骤. 如果不需要任何步骤, 也要明确说明.
如果可能, 要对你支持的每个库版本提供单独版本的文档. 这样可以防止使用者查看过期的信息, 或太新的信息. 如果做不到这一点, 要清楚的标记与 API 中已被重新设计的部分相关的文档章节.
为你的使用者创建角色
对目标受众没有清楚了解的情况下, 创建和评估文档是很困难的. 对阅读你的文档的使用者类型定义几种角色, 会有所帮助.
要考虑使用者的限制条件, 例如他们需要操作的原有的软件栈. 文档的审阅者能够采用这些角色, 让他们的结论更加有意义.
如果你缺乏关于使用者的具体信息, 最好保持悲观态度. 例如, 不要假设他们精通 Kotlin 的最新或最先进的功能. 要让你的代码示例尽可能的简单.
由于时间, 预算的限制, 或者由于保密协议的原因, 无法咨询真实世界的使用者时, 角色会非常有用. 随着时间的推移, 你对使用者的理解更加深入之后, 可以进一步改进角色, 更加准确的满足他们的需求.
尽可能使用示例来创建文档
使用示例的文档, 是向使用者解释基本概念的成本-效益最好的方式之一. 要尽可能的提供简单而且清晰的代码示例, 帮助解释或演示当前正在讨论的主题或概念.
通过 KDoc 文档格式, 你可以在你的文档注释中 使用 Markdown 实现内联的标记. 要在注释中使用内联的代码片段来演示 API 的使用方法. 相关的示例, 请参见协程库的测试调度器的 源代码 和 展示的文档.
提供这样的示例, 可以不必编写冗长的描述文字, 来解释期望的输入, 可能的输出, 以及失败模式. 但是, 每个示例的上下文, 以及它相关的情况, 都必须清晰. 简单的提供一个文件夹, 包含一些没有注释的例子程序, 这样做不能称作文档.
为你的 API 全部创建文档
你的库支持的每个 API 入口点都应该使用 KDoc 编写文档.
Kotlin 的文档引擎, Dokka, 在它的输出中默认只包含 public 声明. 我们在 简单性 中讨论过, 你应该最小化你的 public API, 删除你不希望使用者使用的 public 入口点. 如果存在某些 API, 你不能通过控制可见度来向使用者隐藏它们, 请使用 suppress 指令, 在文档中省略它们.
要对函数的功能进行清晰, 高层级的描述, 以此开始描述入口点. 不要简单的用自然语言把函数签名重复一遍.
例如, 不要说 “接受一个 String
参数, 返回一个 Connection
”, 而应该说 “尝试连接到输入字符串指定的数据库, 如果成功, 则返回 Connection
, 否则抛出 ConnectionTimeoutException
异常”.
要指明每个输入的预期值, 以及对各种输入的行为. 要解释有效值的范围, 以及提供不正确的值时会发生什么结果. 例如, 如果一个字符串输入应该是一个 URL, 要描述清楚, 如果字符串为空, 不合法, 使用不支持的协议, 或指向不存在的位置, 各种情况下会发生什么结果.
要对一个 API 入口点可能抛出的每个异常编写文档. 在一般描述中讨论失败条件, 详细信息留到异常部分讨论. 这样可以增加可读性, 并帮助读者集中注意力. 相反, 要将这些信息有机的包含到一般描述中(译注: 原文疑似有错误). 尽量提供使用示例, 也能够帮助使用者理解如何正确的使用 API.
为 Lambda 表达式参数创建文档
当一个 API 入口点接受 Lambda 表达式作为参数时, 使用者会提供某种功能, 你的库将会为他们执行这个功能. 这就要求提供至少两方面的额外的文档.
首先, 要对如果 Lambda 表达式抛出异常会发生什么编写文档. 要解决以下问题:
这会导致立即失败吗, Lambda 表达式会被重复调用吗, 是否存在回退(fallback) 行为?
如果调用的函数需要退出, 它是否会重新抛出 Lambda 表达式抛出的异常, 或者抛出另一个异常?
如果抛出另一个异常, 它是否包含原来的异常?
此外, 除非函数被声明为内联, 否则要对与并发有关的任何特殊行为创建文档. 确保涵盖了以下内容:
Lambda 表达式是否会在与调用者相同的线程中被调用?
如果 Lambda 表达式不在与调用者相同的线程中被调用, 那么会在哪个线程 (或线程池) 中被调用?
Lambda 表达式的多个副本能够并行运行吗?
还有哪些其它工作可能会正在使用这个线程?
使用者能否指定一个线程让库使用?
当多个 Lambda 表达式被调用时, 对执行顺序提供什么样的保证?
在文档中使用明确的链接
一个 API 入口点完全独立于你的库中的其它功能的情况是非常少见的. 通常来说, 调用必须按照某种顺序进行, 对于执行某个特定的任务存在多种选项, 执行相关任务的入口点也以类似的方式使用. 例如, format
函数和 parse
函数相互之间成为镜像.
应该使用 @see
标记, 或 内部链接, 在你的文档中明确说明这些关系. 这样可以帮助读者将信息 整合 在一起, 构建一个更加整体性的库的认知图景.
尽量保持独立(self-contained)
在描述什么样的输入有效时, 人们倾向于直接参考相关标准, 例如由 W3C, IEEE, 或 Unicode 联盟指定的标准. 尽管提供这些链接可能有所帮助, 但不应该强迫读者为了理解基本信息而去参考外部的规范, 例如空白字符集.
只要有可能, 文档应该保持独立. 它应该为使用者提供足够的信息, 来理解每个 API 入口点典型的使用方法. 通常的使用不会发生的对边缘情况, 你可以让使用者参考外部文档.
使用简单的英语
在创建文档时, 使用简单而且清晰的英语, 是非常重要的. 这可以保证你的内容能够供全球的受众阅读, 包括那些非英语母语的人们. 不要使用复杂的词汇, 行话, 拉丁短语, 或惯用表达, 这些都可能让读者感到困惑. 相反, 要使用直白的语言, 和简洁的句子.
简单的英语还能让文档在需要的时候更容易翻译. 清晰, 没有歧义的文字能够减少误解的风险, 提高整体的可读性.
下一步做什么
如果你还没有阅读过, 请阅读这些章节:
阅读 减少认知复杂度 (Mental Complexity), 学习减少认知复杂度的各种策略.
阅读 向后兼容性, 学习如何维护向后兼容性.