117

我一直在阅读有关 ReST API 的版本控制策略,但它们似乎都没有解决您如何管理底层代码库的问题。

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

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

  • 为 v1 代码库使用源代码控制分支/标记。v1 和 v2 是独立开发和部署的,在必要时使用修订控制合并来将相同的错误修复应用于两个版本 - 类似于在开发主要新版本同时仍支持以前版本时管理本机应用程序的代码库的方式。

  • 使代码库本身了解 API 版本,因此您最终会得到一个包含 v1 客户表示和 v2 客户表示的单一代码库。将版本控制视为解决方案架构的一部分,而不是部署问题——可能使用命名空间和路由的某种组合来确保请求由正确的版本处理。

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

4

4 回答 4

52

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

  • 更改数量少、更改复杂性低或更改计划频率低
  • 与代码库的其余部分在很大程度上正交的更改:公共 API 可以与堆栈的其余部分和平共处,而无需“过度”(对于您选择采用的该术语的任何定义)在代码中分支

我没有发现使用此模型删除不推荐使用的版本过于困难:

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

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

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

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

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

于 2015-05-26T18:31:55.583 回答
13

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

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

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

例如:

客户端 v1 请求:v1 适配 v2 ---> v2 适配 v3 ----> 代码库

客户端 v2 请求:v1 适应 v2(跳过)---> v2 适应 v3 ----> 代码库

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

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

于 2016-04-07T21:06:42.967 回答
6

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

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

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

于 2015-06-02T17:50:15.907 回答
0

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

于 2020-03-20T15:57:28.727 回答