一个 API 应该容易学习和使用,且不易被误用。它还应该随着时间而发展,优秀的设计需要预见并适应这种变化。
Joshua Bloch 曾在 Sun 担任杰出工程师,之后加入谷歌成为首席 Java 架构师。他主导了 Java 平台上的很多功能,包括 Java Collections 框架,java.math 包,assert 机制等。他也是 Effective Java 的作者。
在谷歌 2007 年的一场重要演讲中,软件工程师兼技术作家 Joshua Bloch 强调了 API 是一种极其重要的商业资产。他指出,这主要是因为如果 API 对外开放,客户可能会选择在上面进行大量投资,从而很难改变使用习惯。
Bloch 还警告说,设计糟糕的 API 可能会导致无休止的客户支持电话,极大地阻碍公司的发展。
Bloch 进一步指出,以 API 设计为思考核心,能显著提高编写程序的质量。
即使你作为一个程序员并不直接参与面向公众的 API 开发,实际上你也在持续地创建 API。优秀的编程应该是模块化的,模块间的边界自就是 API。同样,如果你参与的是一个现代化的、分布式的、基于微服务架构的系统,那么服务间的边界也构成了 API,只是架构有所不同。
尽管如此,API 设计仍然是许多程序员面临的一个挑战。那么,一个好的 API 有哪些特点呢?
1、名字至关重要
从宏观角度来看,API 应该易于学习和使用,同时难以被误用。它还需要随着时间的发展而进化,而一个优秀的设计会将此考虑在内。
命名的方式极其重要,因为 API 在实质上是一种需要用户学习的简约语言。
「真正合适的命名可以解决问题并避免误解,因为恰当的命名能够非常明确地表明事物的本质。」SoftIron 首席科学家 Harry Richardson 在接受 The New Stack 采访时表示。
Richardson 特别指出,对于开发者来说,命名塑造了我们的思维模型。
「改变一个已经形成的思维模型是相当困难的工作,这不一定是在代码方面,而是关于我们思考问题方式的方面。」
因此,投入时间去精心挑选一个能够精确描述 API 功能的名称是非常值得的。
作为一个作家的基本工具 —— 字典和词典 —— 在 API 命名过程中也能提供帮助。如果你发现某个名字特别难以确定,这可能意味着它尝试同时承担太多的职责。就像需要将过于复杂的句子分割成更简单的句子一样,当一个模块过于复杂时,也应该考虑将其拆分。
要避免使用让人费解的缩写,并且注意保持命名的一致性。比如,不应该同时使用 getBasicSalary() 和 getBaseSalary() 这样意义相同但命名不一的方法 —— 如果你的 API 中既有 remove() 又有 delete() 方法,使用者能够清楚地知道它们之间的区别吗?
使用的语言需要与组织或供应商公开的其他 API 保持一致性。这种对一致性的追求意味着,实施一定程度的集中化管理会很有帮助。
比如,一些大型企业会把高级技术写作人员的职责扩展到帮助工程团队统一命名方法、属性和字段。
如果你正在开发 REST 风格的系统,独立咨询师兼《掌握 API 架构》一书的合作者 Daniel Bryant 建议参考已有的 API 指南集,这有助于在 API 的行为上实现一致性。对于基于 HTTP 的 API,他推荐考虑使用 OpenAPI,还有其他包括 Atlassian、Google 和 Microsoft 在内的指南。
同时,虽然所有 API 都需要恰当的命名,但这些命名本身是特定于领域的;比如,为量化分析师编写的 API 与为零售商编写的 API 使用的语言会有很大不同。理想情况下,选用的术语应与企业已经使用并至少理解的术语匹配。
为此,Bryant 在对 The New Stack 的讲述中提到,最佳做法是进行用户研究,确保覆盖所有潜在的 API 使用群体。
「QA 团队成员与开发者对于你的 API 应如何运作会有不同的看法,」他说。「我经常见到开发者在没有询问谁会使用它的情况下设计 API,结果暴露了内部的领域模型。」
他推荐从「待完成的工作」(Jobs-to-be-Done)的角度来考虑,比如:你的关键任务是什么?你的工作流是怎样的?你是如何处理它的?你希望如何处理它?最后一个问题至关重要,因为围绕已建立的流程可能会形成惯性。
「如果你能简化复杂事物,你就有可能颠覆人们的世界观,随着系统的演进,通常会出现很好的机会」Bryant说。
2、最小意外原则
你的 API 也应该符合其所用编程语言的惯常用法,并尊重该语言的工作机制。例如,如果 API 要和 Java 配合使用,就应该通过抛出异常来处理错误,而不是像在 C 语言中那样返回错误代码。
API 应遵循最小意外原则。这一原则部分通过对称性实现;比如说,如果你需要添加和删除方法,这些操作应该在适当的地方被一致地实施。
一个优秀的 API 应该仅包含少数几个概念;在学习它时,不应被迫学习太多内容。这并不特指方法、类或参数的数量,而是指 API 所涵盖的概念范围。理想情况下,一个 API 应该只专注于完成一个任务。
也最好避免无谓地添加任何元素。「不确定时就不要添加,」Bloch 这样建议。你通常可以在需要时向 API 中添加某些内容,但一旦 API 被公开,就无法再移除其中的任何部分。
如之前所述,你的 API 需要随时间发展,因此设计的一个关键方面是,在后续过程中能够进行更改而不破坏整体结构。
「归根到底,关键在于 API 应该反映现实,」Richardson表示。「例如,如果一个人可以有多个地址或电话号码,即便你目前只关注一个,也不应该仅允许存在一个地址。忽略现实最终总会带来问题。」
3、API 的粘性
Richardson 指出,仅实施你当前需要的 API 的一部分是一个常见的错误模式。这种做法的风险在于,你可能没有彻底思考 API 的设计,最终导致在其他场景下不可用的结果。
「API 设计需要比任何其他事情投入更多的思考,」Richardson 说,「因为一旦建成,你就无法再对其进行更改。」
第二个问题涉及到封装和实现细节的泄露。
「一旦实现细节泄露,你就无法更改它,」Richardson表示。「因此,你需要考虑,这里进行的操作是什么?这个数据结构的真实含义是什么?」
错误处理通常是被忽略的一个领域。比如,如果你使用数据库作为后端存储,就不应该让 SQL 错误直接暴露出来,因为如果你以后想更改存储机制,这样做就会遇到障碍。
就像软件开发的任何其他方面一样,认为你可以孤立地把自己锁在一个房间里独立完成 API 的开发是一个错误。这样做,你可能会过于坚持自己的设计,即便设计存在问题。最好是像对待任何其他系统一样,频繁地与合作方一起测试你的想法。
在开始编码 API 之前,编写一个简短的规格说明书,向合作方展示它将做什么以及如何工作是个不错的主意。规格说明书保持简短,这样可以增加被阅读的可能性,并防止你一开始就过于投入你的方案。如果你花费几个月时间编写了一个 100 页的规格说明书,你就很难承认它可能并不那么优秀。
文档是被极度低估的一方面,这不仅适用于 API 设计,在整个计算机科学领域都是如此。技术文档编写者经常被低估和低薪,而文档最多被当作事后的补充,这种情况常被「代码即文档」这一危险的观点所体现。
虽然你希望你的 API 易于理解和学习,但它的文档仍极为重要。它应当是完整而全面的,至少包含每个方法的用途、每个字段的作用以及可能的错误条件。
「你希望它能列出所有可能返回的错误代码及其对应的情况」
Richardson 强调。
投入时间来打磨和修正文档,避免诸如使用不容易理解的缩写这样的常见错误。
在开发过程中,继续根据 API 编写示例代码。Bloch 提到,许多开发者在开发过程往往半途而废,但是如果在整个实施过程中持续对 API 进行编码,你将能够真实地感受到它的工作时机和方式。
「这些代码不是无用功,」Bloch强调,「因为它不仅帮助你打造出更优秀的产品,还提供了一套可供其他程序员学习的范例。」
这些示例极为关键,因为它们将被其他开发者不断地复制使用,从而根本性地影响 API 的使用方式。