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

我一直在阅读针对 ReST API 的版本控制策略,它们似乎都没有涉及到如何管理底层代码库。

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

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

  • Use a source control branch/tag for the v1 codebase. v1 and v2 are developed, and deployed independently, with revision control merges used as necessary to apply the same bugfix to both versions - similar to how you'd manage codebases for native apps when developing a major new version whilst still supporting the previous version.

  • Make the codebase itself aware of the API versions, so you end up with a single codebase that includes both the v1 customer representation and the v2 customer representation. Treat versioning as part of your solution architecture instead of a deployment issue - probably using some combination of namespaces and routing to make sure requests are handled by the correct version.

分支模型的明显优势在于,删除旧的 API 版本很容易——只要停止部署适当的分支/标记即可——但如果运行多个版本,最终可能会产生一个非常复杂的分支结构和部署管道。“统一代码库”模型避免了这个问题,但是(我认为?)将使得从代码库中删除不再需要的资源和端点变得更加困难。我知道这可能是主观的,因为不太可能有一个简单的正确答案,但我很好奇组织是如何维护跨多个版本的复杂 API 来解决这个问题的。

11210 次浏览

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

  • A low number of changes, low complexity changes, or low frequency change schedule
  • 在很大程度上与代码库的其余部分正交的更改: 公共 API 可以与堆栈的其余部分和平共存,而不需要在代码中进行“过度”分支(对于您选择采用的术语的任何定义)

我并不觉得用这个模型删除已经过时的版本过于困难:

  • 良好的测试覆盖率意味着删除退役的 API 和相关的支持代码可以确保没有(好吧,最小的)回归
  • 良好的命名策略(API 版本的包名称,或者方法名称中比较丑陋的 API 版本)使得定位相关代码变得容易
  • 横切关注点更加困难; 必须非常仔细地权衡对支持多个 API 的核心后端系统的修改。在某种程度上,版本化后端的成本(参见上面关于“过度”的注释)超过了单个代码库的好处。

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

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

如果您的 API 版本倾向于公开相同类型的资源,但是具有不同的结构表示,例如在您的全名/名称/姓氏示例中,那么这将起作用。如果他们开始依赖于不同的后端计算,情况会变得稍微困难一些,比如,“我的后端服务返回了在公共 API v1中公开的计算错误的复利。我们的客户已经修补了这种不正确的行为。因此,我不能在后端更新该计算,并让它应用到 v2。因此,我们现在需要交出我们的利息计算代码。”幸运的是,这种情况并不常见: 实际上,RESTful API 的使用者更喜欢精确的资源表示而不是 bug-for-bug 的向后兼容性,即使在理论上等幂的 GETted 资源的非破坏性更改中也是如此。

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

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

Yes as you already mentioned - backporting bug fixes will require some effort, but at the same time supporting multiple versions under one source base (with routing and all other stuff) will require you if not less, but at least same effort, making system more complicated and monstrous with different branches of logic inside (at some point of versioning you definetely will come to huge case() pointing to version modules having code duplicated, or having even worse if(version == 2) then...) . 也不要忘记,为了回归的目的,您仍然必须保持测试分支。

关于版本控制策略: 我会从当前版本保留最大 -2版本,不支持旧版本-这会给用户一些动力去移动。

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

As you write, the codebase should be version aware, but a compatibility layer can be used as separate layer. In your example, the codebase can produce resource representation (JSON or XML) with first and last name, but the compatibility layer will change it to have only name instead.

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

例如:

Client v1 request: v1 adapt to v2 ---> v2 adapt to v3 ----> codebase

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

对于响应,适配器的功能只是在相反的方向。如果您正在使用 JavaEE,您可以将 servlet 过滤器链作为适配器链,例如。

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

通常,引入 API 的主要版本会导致您必须维护多个版本,这种情况不会(或不应该)频繁发生。然而,这是无法完全避免的。我认为,总的来说,一个安全的假设是,一个主要的版本,一旦推出,将保持最新的版本相对较长的时间。基于这一点,我更愿意以牺牲重复为代价来实现代码的简单性,因为当我在最新版本中引入更改时,它使我更有信心不会破坏以前的版本。