您如何管理版本化 API 的底层代码库?
How do you manage the underlying codebase for a versioned API?
我一直在阅读 ReST API 的版本控制策略,其中 none 似乎解决了您如何管理底层代码库的问题。
假设我们正在对 API 进行一系列重大更改 - 例如,更改我们的客户资源,以便 returns 将 forename
和 surname
字段而不是单个 name
字段。 (对于这个例子,我将使用 URL 版本控制解决方案,因为它很容易理解所涉及的概念,但这个问题同样适用于内容协商或自定义 HTTP headers)
我们现在在 http://api.mycompany.com/v1/customers/{id}
有一个端点,在 http://api.mycompany.com/v2/customers/{id}
有另一个不兼容的端点。我们仍在发布 v1 API 的错误修复和安全更新,但新功能开发现在都集中在 v2 上。我们如何编写、测试和部署对 API 服务器的更改?我至少可以看到两种解决方案:
对 v1 代码库使用源代码控制 branch/tag。 v1 和 v2 是独立开发和部署的,必要时使用修订控制合并将相同的错误修复应用到两个版本——类似于在开发主要新版本同时仍支持以前版本时管理本机应用程序代码库的方式。
让代码库本身知道 API 版本,这样您最终会得到一个包含 v1 客户表示和 v2 客户表示的代码库。将版本控制视为解决方案架构的一部分,而不是部署问题 - 可能使用命名空间和路由的某种组合来确保请求由正确的版本处理。
分支模型的明显优点是删除旧的 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 向后兼容性,即使在理论上幂等的非破坏性更改中也是如此 GET
ted 资源。
我很想听听您的最终决定。
分支对我来说似乎好多了,我在我的案例中使用了这种方法。
是的,正如您已经提到的那样 - 向后移植错误修复需要一些努力,但同时在一个源代码库下支持多个版本(具有路由和所有其他内容)将需要您,如果不是更少,但至少是相同的努力,使系统变得更加复杂和庞大,内部有不同的逻辑分支(在版本控制的某个时刻,您肯定会变得巨大 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 的主要版本的引入导致您不得不维护多个版本的情况是不会(或不应)经常发生的事件。但是,也不能完全避免。我认为总的来说,一个主要版本一旦引入,将在相对较长的时间内保持最新版本是一个安全的假设。基于此,我更愿意以重复为代价来实现代码的简单性,因为它让我更有信心在最新版本中引入更改时不会破坏以前的版本。
我一直在阅读 ReST API 的版本控制策略,其中 none 似乎解决了您如何管理底层代码库的问题。
假设我们正在对 API 进行一系列重大更改 - 例如,更改我们的客户资源,以便 returns 将 forename
和 surname
字段而不是单个 name
字段。 (对于这个例子,我将使用 URL 版本控制解决方案,因为它很容易理解所涉及的概念,但这个问题同样适用于内容协商或自定义 HTTP headers)
我们现在在 http://api.mycompany.com/v1/customers/{id}
有一个端点,在 http://api.mycompany.com/v2/customers/{id}
有另一个不兼容的端点。我们仍在发布 v1 API 的错误修复和安全更新,但新功能开发现在都集中在 v2 上。我们如何编写、测试和部署对 API 服务器的更改?我至少可以看到两种解决方案:
对 v1 代码库使用源代码控制 branch/tag。 v1 和 v2 是独立开发和部署的,必要时使用修订控制合并将相同的错误修复应用到两个版本——类似于在开发主要新版本同时仍支持以前版本时管理本机应用程序代码库的方式。
让代码库本身知道 API 版本,这样您最终会得到一个包含 v1 客户表示和 v2 客户表示的代码库。将版本控制视为解决方案架构的一部分,而不是部署问题 - 可能使用命名空间和路由的某种组合来确保请求由正确的版本处理。
分支模型的明显优点是删除旧的 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 向后兼容性,即使在理论上幂等的非破坏性更改中也是如此 GET
ted 资源。
我很想听听您的最终决定。
分支对我来说似乎好多了,我在我的案例中使用了这种方法。
是的,正如您已经提到的那样 - 向后移植错误修复需要一些努力,但同时在一个源代码库下支持多个版本(具有路由和所有其他内容)将需要您,如果不是更少,但至少是相同的努力,使系统变得更加复杂和庞大,内部有不同的逻辑分支(在版本控制的某个时刻,您肯定会变得巨大 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 的主要版本的引入导致您不得不维护多个版本的情况是不会(或不应)经常发生的事件。但是,也不能完全避免。我认为总的来说,一个主要版本一旦引入,将在相对较长的时间内保持最新版本是一个安全的假设。基于此,我更愿意以重复为代价来实现代码的简单性,因为它让我更有信心在最新版本中引入更改时不会破坏以前的版本。