API 设计优先与代码优先

随着 API 描述越来越流行,我听到人们问的主要问题是“API 设计优先”还是“代码优先”。这是一个有点误导的问题,因为这不是两个独特的东西,有几种变体。

代码优先,“有时间的时候”写文档

这就是我最初接触 OpenAPI 或 API Blueprint 等 API 描述文档的方式,也是我们的第一本书建议 API 开发人员做事的方式。这在当时可能很有道理,但我很快发现这是一种不成熟的工作流程。

这里的一个问题是,“先写代码,后写文档”将 API 描述视为一种制作 API 参考文档的奇特方式,而这只是 API 描述可以做的 100 件事之一。API 描述是机器可读的文件,包含大量数据和元数据,可用于在早期阶段收集反馈,以便在通过模拟编写 API 之前提高 API 的质量,提供客户端验证和服务器端验证。

首先编写一堆代码,部署东西,让客户上手并得到特别的亲自动手处理等,这是一项非常繁重的工作。当整个阶段完成后,花一个月的时间编写“只会过时”的文档,感觉就像是一项艰巨的任务,大多数企业都很难优先考虑,所以这项任务永远都做不完。

这是我经常听到的借口,说明为什么 WeWork 在 2016 年拥有约 50 名工程师,却在任何时候都无法提供任何文档,却成功构建了约 30 个 API。缺乏文档导致了我所见过的最疯狂的时间和金钱浪费,人们构建了新版本的端点和 API,因为没有人记得代码是如何工作的。由于 API A 动态返回来自 API B 和 API C 的 JSON 块,而没有任何序列化器,因此甚至几乎不可能阅读代码。

“我们稍后再写文档”意味着“我们不会写文档”,等你发现需要它时,已经太晚了。如果你是少数几个能快速完成的人之一,那么让这些文档与代码“同步”是大多数开发人员面临的最大问题。在我关于这个主题的API the Docs演讲中,当我问“这里有谁在努力保持代码和文档同步”时,整个房间大约 80-100 人都举起了手。

有几种方法,但即使你完全使用Dredd或类似的工具来保持同步,还有另一个我们尚未涉及的相当大的问题:你在给客户机会使用它之前就构建了整个 API。

模拟经常被忽视,人们浪费时间和金钱构建无意义的 API,而这些 API 对客户没有帮助。这通常意味着 v2 在 v1 之后很快推出,也许需要 v3,因为会有更多客户参与并提供更多反馈。这通常意味着 API 过于规范化,导致客户需要发出 150 个 HTTP 请求来解决他们的用例,或者资源非常庞大,这意味着在 100 个用户不需要的字段中隐藏着有用的数据。

无论您为 API 构建选择了哪种 API 范式,用例驱动的 API通常都比数据驱动的 API 更有用。让您的用户尽早分享他们的反馈,因为此时更改仍然成本低且容易,而不是在已经投入生产并且更改变得更加复杂时。

先编码,再注释

这种对 API 描述采用代码优先方法的流行变体是为了加快“稍后编写文档”部分的流程,许多 API 开发人员决定使用注释或代码注释以特殊格式在其源代码中散布 API 描述的片段。

有多种工具可用于此目的。在一些严格类型语言中,注释工具包含的信息非常少,大多只有人类可读的描述。可以从代码中推断出基本类型(“字符串”和“整数”)等信息,是否允许为空等。可悲的是,有些人认为这就是他们需要放入描述文档的所有信息。他们忽略了示例值、“电子邮件”或“日期时间”等格式(可以增加验证优势并使文档更有用),以及 OpenAPI 或 JSON Schema 中的其他更高级的功能,如 allOf、oneOf 等。

以注释为首要功能的语言通常对此支持得更好一些,例如 Java。它们拥有大量的注释系统,如果您在其中编写了垃圾内容,则可能会出现语法错误。

class UserController {
@OpenApi(
path = "/users",
method = HttpMethod.POST,
// ...
)
public static void createUser(Context ctx) {
// ...
}
}

其他语言(例如 PHP)依赖于文档块注释,而这只是在文本编辑器中写入无意义的内容。

/**
* @OAGet(path="/2.0/users/{username}",
* operationId="getUserByName",
* @OAParameter(name="username",
* in="path",
* required=true,
* description=Explaining all about the username parameter
* @OASchema(type="string")
* ),
* @OAResponse(response="200",
* description="The User",
* @OAJsonContent(ref="#/components/schemas/user"),
* @OALink(link="userRepositories", ref="#/components/links/UserRepositories")
* )
* )
*/
public function getUserByName($username, $newparam)
{
}

在我看来,这似乎很粗糙,但人们为它辩护的理由如下:“在代码附近添加注释意味着开发人员更有可能保持代码更新”。更有可能并不一定。

使用注释时,您仍然需要使用其中一种方法来确保代码和描述同步,但您必须添加构建步骤以从源代码导出,然后通过 Dredd 或类似程序运行生成的 OpenAPI 文件。或者,您只能希望所有开发人员都记住并且“一切都会好起来”。

这里的反馈循环仍然有点长。它是在你编写了一大堆代码之后出现的,或者你可能将所有路由都写到了一堆空控制器中,并且可以导出 OpenAPI 来创建模拟服务器,但这一切听起来仍然很繁重。还有更多改进要做。

设计优先,代码优先

总体而言,“API 设计优先”是指实质上关闭反馈循环。在编写任何代码之前,您都会获得模拟和文档,因此在相当多的客户确认界面符合他们的需求之前,您无需再对代码进行任何修改,而且由于您已经拥有生成文档所需的一切,因此您不必担心以后再做这件事。

这种设计优先的特定风格仍然存在很多问题,但最近 API 领域的一些大人物一直在提倡这种做法。我认为他们提倡这种做法主要是因为他们厌倦了手写 API 描述:在这里插入关于“数千行 YAML”的常见抱怨。也许他们一开始使用 DSL 来设计东西,然后在完成后切换到注释,再次希望这样“它更有可能保持最新”。

这里的众多错误观点之一是,认为有一个设计阶段,然后你就停止设计,然后是代码编写的时间,之后我们不需要设计新的功能。

无论开发人员是手工编写 API 代码还是根据 API 描述生成代码,设计阶段都是永无止境的。设计是一个循环的生命周期,具有反馈循环,可产生新的资源和端点、新的全局版本或只是新的属性。API 会随着时间的推移而发展,在不收集客户反馈的情况下推出新功能始终是一个坏主意,不仅仅是在初始设计阶段。

我在Meetup上看到一些人使用“不可变服务”取得了成功,他们从 OpenAPI 生成路由、控制器、数据模型、docker 配置,甚至所有 Kubernetes 设置,然后他们只需在空白处插入一些业务逻辑并点击部署即可。当他们需要更改合同时会发生什么?那将是一项全新的服务。不允许更改。计划得足够好,你不需要花很长时间调整它们,然后在需要更改时弃用并替换它们。不可变服务不是一种常见的做事方式,需要大量的纪律才能做好。

对于其他所有人来说,演变更为常见,因为即使人们为其 API 使用主要的全球版本,也会在过程中做出向后兼容的更改(新端点等)。要求您“导入”OpenAPI,然后从那里继续的工具会让您在未来无法设计新功能,即使他们提供了导出 OpenAPI 功能(许多人没有)。

更糟糕的是,许多此类工具都会在云中保留其自己的 API 描述版本,而这些版本可以独立于 Git 存储库中的 API 描述进行更改,这意味着您没有真相来源:您有两个谎言来源。

让我们看一个工作流程,它允许您使用 API 描述作为单一事实来源,它会随着您的代码一起发展。

设计优先,代码演进

这种方法不再将 API 描述文档视为事后的想法或苦差事,因为它们不是。过去可能需要 DSL 才能使编写 OpenAPI 变得容易,但有了Stoplight Studio等令人惊叹的可视化编辑器,使用 DSL 来避免手动编写 YAML 的日子已经一去不复返了。Studio 可让您在本地机器上免费使用 OpenAPI 文件,因此任何人都可以轻松构建强大的描述文档,甚至可以轻松地在多个 API 之间重用模型,这样整个“千行 YAML”的事情就完全消失了。

无论您使用 Studio、DSL 还是手写,都请从空的存储库开始,只使用描述文档。尽早并经常运行模拟服务器,获取客户的反馈,然后在达成一致后提交文档。

然后您就可以开始编写代码了。使用描述文档来支持服务器端验证甚至 API 网关验证的工具可以大大简化您需要编写的代码量。

这不是代码生成,而是使用 API 描述来支持生产验证。您用于呈现文档的相同描述文档现在支持 API 的最复杂方面,并且事情永远不会“不同步”,因为只有一个事实来源。

当客户请求新功能时,您可以轻松添加新端点、引入新属性等,并在开始编写代码之前获得有关新内容的反馈。您永远不会失去这种能力,因此您可以从先设计、再设计、再设计中受益。

这无助于保持响应“同步”,但是由于您的描述文档就在您的存储库中,因此您可以使用它们来大大简化您的单元/集成测试,从而覆盖整个界面。

不要草率地编写描述文档。使用它们来规划一些了不起的事情,并减少后续需要重新编码的工作量。创建使用寿命更长、文档更完善、测试更充分的 API,同时减少开发 API 所花费的总时间。

文章转载自:API设计优化与代码优化

Leave a Reply