写点什么

SaaS 时代,如何确保 API 版本控制的一致性?

作者:Ashwin Raghav

  • 2024-02-13
    北京
  • 本文字数:6023 字

    阅读完需:约 20 分钟

SaaS 时代,如何确保 API 版本控制的一致性?
本文要点
  • SaaS API 的广泛使用暴露出了一个问题,那就是处理主要版本的更新和重大变更的方法是不一致的。

  • API 发布者在解决潜在问题时主要关注 API 的向后兼容性。

  • 鉴于现代应用程序集成了各种 SaaS API,API 发布者不能只考虑基础 API,还需要考虑包括性能、依赖性、wireformat 兼容性等在内的各种问题。

  • 如果不这样做,可能会导致客户不再充分相信版本控制是获取变更信息的可靠工具,迫使 API 发布者支持旧版本来照顾旧版用户,反过来让版本控制的问题更加严重。

  • 这里的目标不是在各个方面都实现兼容性,而是识别出来对你所关心的客户群体最重要的那些方面,并清楚地传达相关信息。

为什么要现在做这件事?


SaaS 平台的广泛流行让现实世界的大多数应用程序都变成了第三方 API 的大杂烩。现代软件构建系统的复杂性、数量飞速增长的库、多语言软件栈和 SaaS 革命加在一起,让软件发行商和使用者都必须对版本控制有一致的理解。


特别是对于 API 重大变更来说,不同人对语义版本控制的解释不一致会导致致命的循环问题,原因有二:


  • 由于跨 API 的重大变更建模方式不可预测,因此使用者对于大版本升级也持谨慎态度,即便这些升级是合理有益的也是如此。

  • 结果,API 开发人员对是否发起重大升级就会犹豫不决,因为他们会面临版本更新缓慢、维护负担增加和新版本采用缓慢的风险。于是他们有时会将重大变更塞进次要或补丁版本里,结果会进一步损害用户的信任度。


流行 API 的发布者通常会对主要版本提供三到四年的支持,等待更新步伐缓慢的用户跟上节奏。


随着生成式 AI 的 SaaS API 持续快速增长,现在我们恰逢一个很好的时机,来回顾到底重大变更包含哪些内容,以及如何在向后兼容性、可升级性与现代化和可迭代性之间做好权衡。


本文讲的是什么事情,不涉及哪些问题


本文讨论了 SemVer 标准中最具争议和最容易被误解的几个部分,即向后兼容性和重大变更。需要注意的是,下文中提到的这两个概念的 引用来源 的解释是可能被修改的。我们的目标是使用现实世界的实际示例,并尽可能引用开源资料来消除歧义。


  • 如果只引入向后兼容的错误修复,则必须增加补丁版本。

  • 如果向公共 API 引入新的向后兼容特性,则必须增加次要版本。

  • 如果公共 API 引入任何向后不兼容的变更,则必须增加主要版本。


大多数 API 发布者实际上只考虑一类重大变更——对 API 签名(参数,包括其类型、返回类型等)的变更,这会要求开发人员重构这些内容与 API 的集成关系。


本文可作为读者的参考,帮助读者了解应该考虑哪些类型的向后兼容性 API/SDK,以及哪些类型应该被有意忽略。我们将提出一些建议,但我们的目标不是定下规矩,而是提供一份如何使用 SemVer 标准来规划 API 演变路线的指南。我们将演示一个不那么明显的重大变更的示例来帮助大家理解。


由于当今大多数 API 都附带客户端库,因此我们的示例是用 Java 编写的,但它们很容易推广到其他语言和非 SDK 上。我们故意不去深入探讨如何使用特定的设计模式或特定的技巧来在 Gradle 或 Maven 之类的地方解决这些问题。我们的目标是让大家对不同类型的破坏性变更都能有办法应对。


我们先从基本的 API 兼容性开始研究,然后再讨论更细致的向后兼容性概念。


API 兼容性


关于向后兼容性,人们最认可的形式是和 API 中的直接变更有关系的。这些变更可能是 API 签名中的变更,例如对参数、其数据类型或返回类型的修改。此类变更可能需要开发人员修改其现有的 API 集成。


// Old versionpublic interface BookService {    Book getBookById(int id);    List<Book> searchBooks(String query);}// New versionpublic interface BookService {    // Method was renamed    Book findBookById(int id);    // New non optional limit parameter would result in compile time error    List<Book> searchBooks(String query, int limit);}
复制代码


ABI 兼容性


除了 API 兼容性之外,许多编程语言还会考虑 ABI(应用程序二进制接口)兼容性。比如说在 Java 中,即使方法签名发生变更,库也可能会保持 API 兼容性,但 ABI 兼容性可能就没了。


// Old versionpublic class Example {    public void doSomething(int value) {        // Implementation    }}// New versionpublic class Example {    public void doSomething(Integer value) {        // Implementation    }}
复制代码


在这个例子中:API 是兼容的:从这个库的版本 1 切换到版本 2 时,使用 Example 类的 doSomething 方法的程序不需要修改(假设它们传递一个整型值或 int 变量,该变量在版本 2 中自动装箱为 Integer)。


ABI 是不兼容的:如果不重新编译,针对这个库的版本 1 编译的程序将无法继续使用它的版本 2。这是因为方法签名已变更:它现在采用 Integer 对象而不是 int。在 JVM 级别,方法签名包括参数类型,因此 doSomething(int) 和 doSomething(Integer) 在二进制级别被视为不同的方法。


应用程序二进制接口(ABI)兼容性的概念可能看起来有些陌生,或者跟基础 API 没多大关系。但它的用途越来越广泛,特别是当你的实现利用外部函数接口(FFI)与底层原生平台通信时更是如此。FFI 允许用一种编程语言编写的程序调用另一种语言的函数并使用后者编写的服务,而且往往是比较底层的函数和服务。所以一定要确保此类场景中的 ABI 兼容性才能维护软件的完整性和功能,因为它依赖于跨不同编程环境的一致数据结构、函数签名和调用约定。


伙伴 API 兼容性


SaaS 平台越来越普遍地提倡某种“共用效果更佳”的互操作性,强调这些平台的产品结合使用时能够增强功能,并无缝集成。


考虑一个要用到一个支付 API 和一个分析 API 的场景。在这个例子中,这些 API 的客户端库被设计为能够自动实现良好的协同效果。当系统通过支付 API 处理交易时,会自动触发客户端事件的生成:


// Old versionpublic interface AnalyticsClient {    //Not a public API. For partner use only    public void logPaymentSuccess();}public class PaymentsClient {    public void processPayment(AnalyticsClient a) {        a.logPaymentSuccess();    }}AnalyticsClient analytics = new AnalyticsClientImpl()PaymentsClient payments = new PaymentsClient()payments.processPayment(analytics);
复制代码


// New versioninterface AnalyticsClient {    // Not a public API. For partner use only       // Introduce a backward incompatible change that was informed to the partner    public void logPaymentSuccess(String source, String authToken);}AnalyticsClient analytics = new AnalyticsClientImpl()PaymentsClient payments = new PaymentsClient()// will not compile unless the payments library is also upgraded.payments.processPayment(analytics);

复制代码


就算负责分析工作的合作方提前通知负责支付的一方,告诉他们伙伴 API 已经损坏了,这两个库的最终用户还是需要同时升级分析 API 和支付 API,以防服务出现任何中断。现代构建系统在这方面已经有了很多改进,可以更好地处理依赖关系并建立版本冲突解决策略。然而,我们还是需要仔细分析和理解这些类型的 API 变更及其影响。强制客户同时升级多个依赖项的升级体验可能还是会被视为重大变更。


性能兼容性


公共 API 是传递合约的方式。在现实世界中,API 的使用者对合约的解释各不相同。我们应该设计出鼓励“即发即忘”调用模式的 API(日志记录、计数器等)。在这样的情况下,与实现相关的变更一般不会被视为破坏。但任何明显增加调用延迟的行为都可能导致相当大的行为变化和客户流失的后果。


private static final Logger simpleLogger = new Logger() {        @Override        public void log(String message) {            System.out.println(message); // Just a simple console logger for this example.        }    };private static final Logger simpleLogger = new Logger() {        @Override        public void log(String message) {
System.out.println(message);
//Expensive IO Operation writeLogToFile(message); } };
复制代码


Wireformat 兼容性


开发 API 时,为模式演化和数据序列化选择合适的工具也是非常关键的。并非每个 API 都可以灵活地支持进程间通信(IPC)和远程过程调用(RPC)格式,这时它们就只会用 JSON。双向流、繁琐的 API 和处理大型负载等场景需要更针对性的序列化方法。这可能会带来一类难以察觉的破坏性变更。


以一个用于本地日志系统的 SaaS API 为例。升级这个本地 API 可能不会变更其接口。但如果升级改变了数据格式(例如将浮点数表示为字符串),则可能需要同时更新所有客户端应用程序。这样的升级往往很难协调,并且可能演变成重大变更。


Protobuf、Flatbuffers、Avro 和 Parquet 等工具是 API 开发人员的好帮手。它们让我们可以更好地理解模式演变及其与数据传输方法的集成关系。



系统级兼容性


在现代开发工作中,SDK 经常部署在各种且难以预测的环境中,移动操作系统就是一个例子。这种情况通常会导致 API 的实现细节暴露出来,而这些细节原本不应该成为公共接口的一部分。


一个常见的场景是使用 Android SDK,开发人员需要指定一个 minSDKVersion。这代表 SDK 兼容的最低 Android 版本。如果 SDK 更新包含了仅在更高版本的 Android 中可用的新的系统级 API,那么 SDK 清单中的 minSDKVersion 也需要跟上去。虽然这种变更在技术上是必要的,但开发人员并不总是将其视为 API 破坏性变更。


然而,这可能会导致针对旧版本的 API 的使用者遭遇冲突情况,遇到臭名昭著的“minSdkVersion x 不能小于库中声明的版本 x+n”的错误。于是,API 使用者被迫提高他们的 minSDKVersion 版本号,导致他们失去一部分仍在使用旧 Android 版本的用户群。


可降级性


如果升级到较新 API 版本的使用者无法返回到以前的版本,那么这可能就是一个重大变更。例如,如果升级版本重命名了数据库列后旧版本没法理解,那就没办法回滚或降级 API 了。这里有一个与流行的 Android 版 Google Firebase SDK 相关的真实示例。


SDK 无法降级可能意味着整个应用程序都无法回滚,大大增加了开发者引入升级版本时的风险。此类变更可能需要被视为破坏性变更。


    // v1     public void init() {        // perform necessary database migrations    }    // v2    // executing this version of init will make it impossible to downgrade to older versions that expect the old column name    public void init() {        // perform necessary database migrations        execute("ALTER TABLE table_name RENAME COLUMN user TO username;");    }
复制代码


数据收集 / 存储 / 保留兼容性


如果你的新版 API 变更了它们隐式收集、存储或处理的数据,则明智的做法是将其作为重大变更推给用户。无意中收集的数据会对使用者产生现实的法律影响,并且可能会影响软件的分发行为。大多数 SaaS 提供商都应公开告知其数据收集政策是如何间接影响与 App Store、PlayStore 和世界各地的监管机构相关的应用隐私保证的信息。


依赖兼容性


你的 SDK 的依赖项也会引入破坏性变更。除非你“隐藏”依赖项并将它们打包到你的发行版中(但这并不一定是最好的办法,甚至可能无法做到!),否则你的 SDK 依赖项中的符号也是应用程序命名空间的一部分。如果 API 使用了一个库,使用这个 API 的应用也用了这个库,但用的是一个和前者不兼容的版本,这种问题就会变得特别麻烦,带来难以解决的符号冲突。关键在于我们应该只依赖高度稳定的库,因为这种库很少发生重大变化,就算有也是经过深思熟虑的。


一条血泪教训是永远不要让你的公共 API 暴露某个依赖项的 API。这可能是灾难性的,结果你的 API 的发展也必须跟着你的依赖项的节奏来。考虑以下场景:


import org.joda.time.DateTime;public interface DateProcessorInterface {    void processDate(DateTime jodaDate);}
复制代码


每当这里用到的日期库引入重大变更时,对于那些直接依赖这个日期库的客户来说,你的 API 也相当于引入重大变更了。因此,请仔细选择你的依赖项,考虑对它们 shading 或重新命名空间,还要更新到最新版本。


隐式合约兼容性


虽然你的 API 代表一份具体的合约,但客户会从他们的角度解释这份合约。即使打破的是这种隐式合约也会导致不愉快的经历。考虑以下示例:


auth.getSignedInUser(email, password)        .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {            @Override            public void onComplete(@NonNull Task<AuthResult> task) {                // Callback fires immediately if the session is already in memory                if (task.isSuccessful()) {
} else {
} } });
复制代码


虽然你可能在 API 注释里写的是长时间运行的异步操作,但可能会有客户注意到你的 API 在大多数情况下会立即返回,所以就没在他们的 UI 里设计进度条了。作为 SaaS 提供商,你有责任不引入可能会导致回调不会立即触发的实现变更,以免破坏此类隐式合约。虽然你可能会发现修改这些隐含的协议是合理的举措,但请注意,客户可能会将这些变更视为错误。适当的文档和构建工具(例如 lint 检查)可以帮助我们识别此类隐式合约,并帮助 API 使用者避免痛苦的迁移过程。


总    结


SaaS API 面临的挑战是复杂且不断变化的。本文讨论的重点是我们必须细致地了解版本控制和重大变更的影响。


首先,处理主要版本更新和跨 SaaS API 的重大变更时的任何不一致都可能造成重大错误。API 发布者通常只关注 API 兼容性,而忽视了更广泛的影响。现代应用程序中往往集成了多个 SaaS API,这就需要 API 开发者建立更广阔的视野,涵盖 API 功能和行为的各个方面。


其次,人们越来越认识到重大变更不仅仅涉及 API 签名的变更。它包含一系列因素,从 ABI 和 wireformat 兼容性到系统级变更和隐含的合约预期等等。这些变更如果管理不当,可能会削弱客户对版本控制的信任,认为它不再是了解变更信息的可靠工具,从而迫使发布商支持过时的版本,长期停留在不良的版本控制实践中。


本文给出的示例表明,在各个方面都实现完全向后兼容性并不总是可行或可取的。真正的目标是识别出来对你的客户群最重要的变更并将这些信息清楚地传达给他们。这就要求我们在保持向后兼容性、鼓励可升级性以及拥抱现代化和迭代之间作出精妙的平衡。


本文提供的示例说明了 API 演变的复杂性。这些不仅仅是技术挑战,还涉及对客户需求和期望的深刻理解。在深入了解兼容性和重大变更的各个层面的影响后,API 发布者就可以做出明智的决策,结果不仅可以改进他们的产品,还可以在用户群中培养信任关系和忠诚度。

的专家。


原文链接:

https://www.infoq.com/articles/breaking-changes-are-broken-semver

2024-02-13 08:0012753

评论

发布
暂无评论
发现更多内容

愿当传播通信技术火种的普罗米修斯

融云 RongCloud

通信云 技术大会

如何处理工作与生活之间的冲突?

石云升

28天写作 职场经验 12月日更

复杂场景下,通信云服务商如何赋能开发者

融云 RongCloud

音视频 通信云 语音社交

前端领域的数据状态统一管理机制

鲸品堂

大前端

低代码实现探索(三)后台模型执行设计

零道云-混合式低代码平台

安全第二话

张老蔫

28天写作

风口之下,音视频应用出海的三大机遇

融云 RongCloud

音视频 通信 出海

视镜:华为云媒体质量管理最新实践

华为云开发者联盟

音视频 华为云 媒体质量 视镜

6.《重学JAVA》--数据类型

杨鹏Geek

Java 25 周年 28天写作 12月日更

【架构实战营】模块六

衣谷

架构实战营

架构实战营:模块一作业

Geek_93ffb0

「架构实战营」

TypeScript 之模板字面量类型

冴羽

JavaScript typescript 翻译 大前端

搭积木一样实现语音社交软件开发

融云 RongCloud

开发者 通信云 语音社交

低代码平台是伪需求?不好意思,你的同行已经靠它完成转型升级了!

J2PaaS低代码平台

低代码 低代码开发 低代码开发平台 低代码平台

基于云的技术架构设计实践 - 第3篇

hackstoic

签约计划第二季 业务安全

焱融 YRCloudFile 连获两项重量级认证,展现强劲存储实力!

焱融科技

云计算 分布式 云原生 高性能 文件存储

架构训练营-模块一作业

zhongwy

架构实战营

网络安全之SQL注入深入分析

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 SQL注入

漏洞检测方法如何选?详解源代码与二进制SCA检测原理

华为云开发者联盟

安全 漏洞 软件成分分析 源代码SCA检测 二进制SCA检测

实用机器学习笔记六:数据清理

打工人!

机器学习 算法 学习笔记 12月日更 实用机器学习

青藤:一招制敌!微隔离,让勒索软件不再横行

青藤云安全

选课排课软件原生开发选课排课小程序模式源码开发

风行无疆

浪潮云跻身中国政务大数据管理平台市场领导者位置

云计算 云计算运维

华为云数据库GaussDB(for Influx)揭秘第二期:解密GaussDB(for Influx)的数据压缩

华为云开发者联盟

数据库 时序数据库 GaussDB(for Influx) 时序数据 数据压缩

清空数组的几个方式

CRMEB

基于RPA的自动化优先,正在成为广大组织的主流管理思维

王吉伟频道

RPA 机器人流程自动化 业务流程管理 自动化优先 业务流程自动化

如何对Android 11进行网络状态监听

Changing Lin

12月日更

7个连环问揭开java多线程背后的弯弯绕

华为云开发者联盟

Java 多线程 工作内存 主内存

如何建成有效的前端效能度量体系

benyasin

大前端 研发效能 研发度量 研发提效

直播:开发者如何抵达元宇宙

融云 RongCloud

开发者 元宇宙

低代码实现探索(四)前端组件核心模型

零道云-混合式低代码平台

SaaS 时代,如何确保 API 版本控制的一致性?_后端_InfoQ精选文章