如何管理版本化API的基础代码库?


104

我一直在阅读有关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的组织如何解决此问题。


41
感谢您提出这个问题!我无法相信会有更多人没有回答这个问题!我厌倦了每个人都对版本如何进入系统有意见,但是似乎没人能解决将版本分配到适当代码的真正难题。到目前为止,对于这个看似普遍的问题,应该至少有一系列公认的“模式”或“解决方案”。关于“ API版本控制”的问题如此之多。确定如何接受版本是FRIKKIN SIMPLE(相对)!一旦进入,在代码库中处理它就很困难!
arijeet 2015年

Answers:


45

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

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

我发现使用此模型删除过时的版本并不困难:

  • 良好的测试覆盖率意味着淘汰淘汰的API和相关的支持代码可确保没有(很好,最少)的回归
  • 良好的命名策略(API版本的程序包名称,或方法名称中的API版本较丑)使查找相关代码变得容易
  • 跨部门关注更加困难;必须非常仔细地权衡对核心后端系统进行的修改以支持多个API。在某些时候,版本化后端的成本(请参见上面对“过多”的评论)超过了单个代码库的收益。

从减少共存版本之间的冲突的角度来看,第一种方法当然更简单,但是维护独立系统的开销往往超过减少版本冲突的好处。话虽如此,站起来一个新的公共API堆栈并开始在单独的API分支上进行迭代是非常简单的。当然,世代相传几乎马上就到来了,分支变成了混乱的合并,合并冲突解决方案以及其他类似的乐趣。

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

如果您的API版本倾向于公开相同种类的资源,但是具有不同的结构表示形式(如您的全名/全名/姓氏示例),则此方法将起作用。如果他们开始依赖于不同的后端计算,将变得更加困难,例如,“我的后端服务返回了错误计算的复利,该复利已经暴露在公共API v1中。我们的客户已经修复了此错误行为。因此,我无法更新在后端进行计算,并将其应用到v2。因此,我们现在需要分叉兴趣计算代码。” 幸运的是,这些情况很少见:实际上,RESTful API的使用者更喜欢准确的资源表示形式,而不是逐个bug的向后兼容性,即使是在理论上同等的GET资源上进行的不间断更改。

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


5
只是好奇,在源代码中,您是否在v0和v1之间复制了没有改变的模型?还是您有v1使用某些v0模型?对我来说,如果看到v1在某些领域使用v0模型,我会感到困惑。但另一方面,它将减少代码膨胀。对于处理多个版本,我们是否只需要接受并接受从未更改的模型的重复代码?
EdgeCaseBerg

1
我的回忆是,我们的源代码对模型进行了版本控制,而与API本身无关,例如,API v1可能使用Model V1,而API v2也可能使用Model V1。基本上,公共API的内部依赖性图包括公开的API代码以及后端“实现”代码,例如服务器和模型代码。对于多个版本,我曾经使用过的唯一策略是复制整个堆栈-混合方法(复制了模块A,对模块B进行了版本化……)似乎非常令人困惑。YMMV当然。:)
Palpatim '17

2
我不确定我是否遵循第三种方法的建议。是否有任何公开的示例代码结构?
Ehtesh Choudhury

13

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

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

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

例如:

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

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

为了响应,适配器仅在相反方向上起作用。如果使用的是Java EE,则可以将servlet筛选器链作为适配器链。

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


如果整个基础代码库都已更改,则很难保证兼容性。为漏洞修复版本保留旧代码库要安全得多。
马塞洛·坎托斯

5

分支对我来说似乎好得多,我在这种情况下使用了这种方法。

是的,正如您已经提到的那样-向后移植错误修复将需要一些努力,但是同时在一个源基础上支持多个版本(包括路由和所有其他内容)将需要您(如果不是更少的话,但至少是同样的努力),从而使系统变得更加强大。复杂而又庞杂的内部具有不同的逻辑分支(在版本控制的某个时刻,您会明确地case()指向具有重复代码甚至更差代码的版本模块if(version == 2) then...)。同样不要忘记,出于回归目的,您仍然必须保持测试分支。

关于版本控制政策:我会保留当前最多-2个版本,而不再支持旧版本-这将为用户提供一些动力。


我现在正在考虑在单个代码库中进行测试。您提到测试总是需要分支的,但是我认为针对v1,v2,v3等的所有测试也可以存在于同一解决方案中,并且所有测试都可以同时运行。我想带属性的装饰测试这说明他们支持什么版本:比如 [Version(From="v1", To="v2")][Version(From="v2", To="v3")][Version(From="v1")] // All versions 刚才探讨当中,听过有人这样做?
Lee Gunn

1
好吧,三年之后,我知道对原始问题:D没有确切的答案。它非常依赖项目。如果您有能力冻结API并仅维护它(例如,错误修正),那么我仍然会分支/分离相关的代码(与API相关的业务逻辑+测试+其余端点),并将所有共享的东西放在单独的库中(具有自己的测试) )。如果V1将与V2共存一段时间,并且功能工作仍在进行中,那么我也将它们放在一起进行测试(覆盖V1,V2等,并进行相应命名)。
edmarisov

1
谢谢。是的,这似乎是一个自以为是的空间。我将首先尝试一种解决方案方法,然后看看它如何进行。
Lee Gunn

0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.