我们都喜欢拥有闪亮的新工具,但讨厌不断更新它们的苦差事。这适用于任何事物:操作系统、应用程序、API、Linux 软件包。当我们的代码因为更新而停止工作时,这是痛苦的,而当更新甚至不是我们发起的时,痛苦会加倍。
在 Web API 开发中,您始终面临着每次新更新都会破坏用户代码的风险。如果你的产品是API,那么每次这些更新都会令人恐惧。 Monite 的主要产品是我们的 API 和白标 SDK。我们是一家 API 优先的公司,因此我们非常注意保持我们的 API 稳定且易于使用。因此,重大变更的问题几乎是我们优先考虑的问题。
常见的解决方案是向您的客户发出弃用警告并很少发布重大更改。突然之间,您的发布现在可能需要几个月的时间,并且某些功能必须保持隐藏甚至不合并,直到每个下一个版本为止。这会减慢您的开发速度并迫使您的用户每隔几个月更新一次集成。
如果您加快发布速度,您的用户将不得不过于频繁地更新他们的集成。如果延长发布之间的时间,公司的进展就会变慢。你给用户带来的越不方便,对你来说就越方便,反之亦然。这当然不是最佳方案。我们希望按照自己的节奏前进,而不会对现有客户造成任何破坏,而常规弃用方法是不可能做到这一点的。这就是我们选择替代解决方案的原因:API 版本控制。
这是一个非常简单的想法:随时发布任何重大更改,但将它们隐藏在新的 API 版本下。它为您提供了两全其美的优势:用户的集成不会经常被破坏,并且您将能够以您喜欢的任何速度移动。用户可以随时迁移——没有任何压力。
考虑到这个想法的简单性,它对于任何公司来说都是完美的。这就是您期望在典型的工程博客中读到的内容。遗憾的是,事情没那么简单。
API 版本控制非常困难。一旦你开始实施它,它虚幻的简单性很快就会消失。遗憾的是,互联网从未真正警告过您,因为有关该主题的资源少得惊人。他们中的绝大多数人争论 API 版本应该放在哪里,但只有少数文章试图回答:“我们如何实现它?”。最常见的是:
单独的部署可能会变得非常昂贵且难以支持,复制单个路由不能很好地扩展到大型更改,并且复制整个应用程序会创建大量额外的代码,以至于在几个版本之后您就会开始淹没其中。
即使您尝试选择最便宜的,版本控制的负担很快就会赶上。一开始,它会感觉很简单:在这里添加另一个模式,在那里添加业务逻辑的另一个分支,并在最后复制一些路由。但是,如果版本足够多,您的业务逻辑将很快变得难以管理,许多开发人员会弄错应用程序版本和 API 版本,并开始对数据库中的数据进行版本控制,您的应用程序将变得无法维护。
您可能希望永远不会同时拥有超过两个或三个 API 版本;您每隔几个月就可以删除旧版本。如果您只支持少数内部消费者,确实如此。但您组织之外的客户不会享受每隔几个月就被迫升级的体验。
API 版本控制很快就会成为基础设施中最昂贵的部分之一,因此提前进行深入研究至关重要。如果您只支持内部消费者,那么使用 GraphQL 等工具可能会更简单,但它很快就会变得与版本控制一样昂贵。
如果您是一家初创公司,明智的做法是将 API 版本控制推迟到开发的后期阶段,此时您有资源来正确执行此操作。在那之前,弃用和附加变更策略可能就足够了。您的 API 并不总是看起来很棒,但至少您可以通过避免显式版本控制来节省大量资金。
经过几次尝试和许多错误后,我们正处于十字路口:我们上面提到的先前版本控制方法的维护成本太高。经过我们的努力,我设计了以下完美版本控制框架所需的要求列表:
遗憾的是,我们现有的方法几乎没有其他选择。这时我想到了一个疯狂的想法:如果我们尝试构建一些复杂的、适合工作的东西——比如 Stripe 的 API 版本控制,会怎么样?
经过无数次实验,我们现在拥有了 Cadwyn:一个开源 API 版本控制框架,它不仅实现了 Stripe 的方法,而且显着构建于其之上。我们将讨论它的 Fastapi 和 Pydantic 实现,但核心原则与语言和框架无关。
所有其他版本控制方法的问题是我们重复太多。当我们的合约只有一小部分被破坏时,为什么我们要复制整个路由、控制器甚至应用程序?
借助 Cadwyn,每当 API 维护者需要创建新版本时,他们都会将重大更改应用于最新的架构、模型和业务逻辑。然后他们创建一个版本更改——一个封装新版本和先前版本之间所有差异的类。
例如,假设以前我们的客户可以创建一个具有地址的用户,但现在我们希望允许他们指定多个地址而不是单个地址。版本更改如下所示:
class ChangeUserAddressToAList(VersionChange): description = ( "Renamed `User.address` to `User.addresses` and " "changed its type to an array of strings" ) instructions_to_migrate_to_previous_version = ( schema(User).field("addresses").didnt_exist, schema(User).field("address").existed_as(type=str), ) @convert_request_to_next_version_for(UserCreateRequest) def change_address_to_multiple_items(request): request.body["addresses"] = [request.body.pop("address")] @convert_response_to_previous_version_for(UserResource) def change_addresses_to_single_item(response): response.body["address"] = response.body.pop("addresses")[0]
Cadwyn 使用 instructions_to_migrate_to_previous_version 为较旧的 API 版本的模式生成代码,这两个转换器函数是让我们能够维护任意多个版本的技巧。该过程如下所示:
我们的 API 维护人员创建版本更改后,他们需要将其添加到我们的 VersionBundle 中,以告诉 Cadwyn 此 VersionChange 将包含在某个版本中:
VersionBundle( Version( date(2023, 4, 27), ChangeUserAddressToAList ), Version( date(2023, 4, 12), CollapseUserAvatarInfoIntoAnID, MakeUserSurnameRequired, ), Version(date(2023, 3, 15)), )
就是这样:我们添加了一项重大更改,但我们的业务逻辑仅处理单个版本 - 最新版本。即使我们添加了数十个 API 版本,我们的业务逻辑仍然不受版本控制逻辑、不断重命名、if 和数据转换器的影响。
版本更改取决于 API 的公共接口,我们几乎从不向现有 API 版本添加重大更改。这意味着一旦我们发布了该版本,它就不会被破坏。
因为版本更改描述了版本内的重大更改,并且旧版本中没有重大更改,所以我们可以确定我们的版本更改是完全不可变的 - 它们永远不会有理由更改。不可变实体比业务逻辑的一部分更容易维护,因为它总是在发展。版本更改也会被一个接一个地应用——在版本之间形成一系列转换器,可以将任何请求迁移到任何较新版本,并将任何响应迁移到任何旧版本。
API 合约比模式和字段复杂得多。它们由所有端点、状态代码、错误、错误消息,甚至业务逻辑行为组成。 Cadwyn 使用我们上面描述的相同 DSL 来处理端点和状态代码,但错误和业务逻辑行为是另一回事:它们不可能使用 DSL 来描述,它们需要嵌入到业务逻辑中。
This makes such version changes much more expensive to maintain than all others because they affect business logic. We call this property a "side effect" and we try to avoid them at all costs because of their maintenance burden. All version changes that want to modify business logic will need to be marked as having side effects. It will serve as a way to know which version changes are "dangerous":
class RequireCompanyAttachedForPayment(VersionChangeWithSideEffects): description = ( "User must now have a company_id in their account " "if they want to make new payments" )
It will also allow API maintainers to check that the client request uses an API version that includes this side effect:
if RequireCompanyToBeAttachedForPayment.is_applied: validate_company_id_is_attached(user)
Cadwyn has many benefits: It greatly reduces the burden on our developers and can be integrated into our infrastructure to automatically generate the changelog and improve our API docs.
However, the burden of versioning still exists and even a sophisticated framework is not a silver bullet. We do our best to only use API versioning when absolutely necessary. We also try to make our API correct on the first try by having a special "API Council". All significant API changes are reviewed there by our best developers, testers, and tech writers before any implementation gets moving.
Special thanks to Brandur Leach for his API versioning article at Stripe and for the help he extended to me when I implemented Cadwyn: it would not be possible without his help.
以上是Monite 的 API 版本控制的详细内容。更多信息请关注PHP中文网其他相关文章!