您如何管理版本化 API 的底层代码库?

How do you manage the underlying codebase for a versioned API?

我一直在阅读 ReST API 的版本控制策略,其中 none 似乎解决了您如何管理底层代码库的问题。

假设我们正在对 API 进行一系列重大更改 - 例如,更改我们的客户资源,以便 returns 将 forenamesurname 字段而不是单个 name 字段。 (对于这个例子,我将使用 URL 版本控制解决方案,因为它很容易理解所涉及的概念,但这个问题同样适用于内容协商或自定义 HTTP headers)

我们现在在 http://api.mycompany.com/v1/customers/{id} 有一个端点,在 http://api.mycompany.com/v2/customers/{id} 有另一个不兼容的端点。我们仍在发布 v1 API 的错误修复和安全更新,但新功能开发现在都集中在 v2 上。我们如何编写、测试和部署对 API 服务器的更改?我至少可以看到两种解决方案:

分支模型的明显优点是删除旧的 API 版本很简单 - 只需停止部署适当的 branch/tag - 但如果你是 运行 多个版本,您最终可能会得到一个非常复杂的分支结构和部署管道。 "unified codebase" 模型避免了这个问题,但是(我认为?)当它们不再需要时,会更难从代码库中删除已弃用的资源和端点。我知道这可能是主观的,因为不太可能有一个简单的正确答案,但我很想知道跨多个版本维护复杂 API 的组织是如何解决这个问题的。

你提到的两种策略我都用过。在这两种方法中,我更喜欢第二种方法,它在支持它的用例中更简单。也就是说,如果版本控制需求很简单,那么就选择更简单的软件设计:

  • 更改次数少、更改复杂度低或更改频率低
  • 大部分与代码库的其余部分正交的更改:public API 可以与堆栈的其余部分和平共存,而不需要 "excessive"(对于任何定义的您选择采用的那个术语)在代码中分支

我发现使用此模型删除已弃用的版本并不太困难:

  • 良好的测试覆盖率意味着删除已退休的 API 和相关的支持代码确保没有(好吧,最小的)回归
  • 良好的命名策略(API 版本包名称,或者更难看,方法名称中的 API 版本)使相关代码的定位变得容易
  • 横切关注点更难;必须非常仔细地权衡对核心后端系统的修改以支持多个 API。在某些时候,版本控制后端的成本(参见上面 "excessive" 的评论)超过了单个代码库的好处。

从减少共存版本之间冲突的角度来看,第一种方法当然更简单,但维护独立系统的开销往往超过减少版本冲突的好处。也就是说,建立一个新的 public API 堆栈并开始在单独的 API 分支上迭代非常简单。当然,几代人的损失几乎立即就开始了,分支机构变成了一堆乱七八糟的合并、合并冲突解决方案,以及其他类似的乐趣。

第三种方法是在架构层:采用 Facade 模式的变体,并将您的 API 抽象为 public 面向的版本化层,这些层与适当的 Facade 实例对话,这反过来通过它自己的一组 APIs 与后端对话。您的 Facade(我在之前的项目中使用了一个适配器)变成了它自己的包,自包含且可测试,并允许您独立于后端迁移前端 APIs,并且相互独立。

如果您的 API 版本倾向于公开相同类型的资源,但具有不同的结构表示,这将起作用,如您的 fullname/forename/surname 示例。如果他们开始依赖不同的后端计算,它会变得稍微困难​​一些,例如,"My backend service has returned incorrectly calculated compound interest that has been exposed in public API v1. Our customers have already patched this incorrect behavior. Therefore, I cannot update that computation in the backend and have it apply until v2. Therefore we now need to fork our interest calculation code." 幸运的是,这些往往很少见:实际上,RESTful API 的消费者更喜欢准确的资源表示bug-for-bug 向后兼容性,即使在理论上幂等的非破坏性更改中也是如此 GETted 资源。

我很想听听您的最终决定。

分支对我来说似乎好多了,我在我的案例中使用了这种方法。

是的,正如您已经提到的那样 - 向后移植错误修复需要一些努力,但同时在一个源代码库下支持多个版本(具有路由和所有其他内容)将需要您,如果不是更少,但至少是相同的努力,使系统变得更加复杂和庞大,内部有不同的逻辑分支(在版本控制的某个时刻,您肯定会变得巨大 case() 指向具有重复代码的版本模块,或者更糟糕的 if(version == 2) then...)。 也不要忘记,出于回归目的,您仍然必须保持测试分支。

关于版本控制政策:我会保留当前的最大 -2 个版本,弃用对旧版本的支持 - 这会给用户移动一些动力。

对我来说,第二种方法更好。我已经将它用于 SOAP Web 服务,并计划也将它用于 REST。

在您编写时,代码库应该是版本感知的,但是兼容层可以用作单独的层。在您的示例中,代码库可以生成具有名字和姓氏的资源表示(JSON 或 XML),但兼容层会将其更改为只有名称。

代码库应该只实现最新版本,比如 v3。兼容层应该在最新版本 v3 和支持的版本(例如 v1 和 v2)之间转换请求和响应。 兼容层可以为每个支持的版本都有一个单独的适配器,可以作为链连接。

例如:

客户端v1请求:v1适配v2 ---> v2适配v3 ----> codebase

客户端v2请求:v1适配v2(跳过)---> v2适配v3 ----> codebase

对于响应,适配器只是在相反的方向上起作用。如果您使用 Java EE,您可以将 servlet 过滤器链作为适配器链。

删除一个版本很简单,删除相应的适配器和测试代码。

通常,API 的主要版本的引入导致您不得不维护多个版本的情况是不会(或不应)经常发生的事件。但是,也不能完全避免。我认为总的来说,一个主要版本一旦引入,将在相对较长的时间内保持最新版本是一个安全的假设。基于此,我更愿意以重复为代价来实现代码的简单性,因为它让我更有信心在最新版本中引入更改时不会破坏以前的版本。