【融云分析】如何保障 API 设计的稳定性

计算机行业有句名言 —— 计算机科学领域的任何问题,均可以经过增长一个间接的中间层来解决。php

当前的计算机领域,不管广度仍是深度,已经没有一我的能彻底掌握了。可是,经过各类中间层的组合使用,咱们不须要了解其内部细节,也能够像搭积木同样,开发出各类有趣的服务和应用。
而各个中间层之因此能组合工做,正是由于你们都经过定义好的 API 交互和通讯。每一个模块在对外提供通过抽象 API 的同时,也须要使用其余模块的 API 做为自身运行的基础。linux

今天咱们来聊聊融云在设计 API 过程保障稳定性的一些实践。git

无处不在的 API
API(Application Programming Interface) 又称为应用编程接口。github

而接口,本质能够理解为契约,一种约定。
计算机接口的概念起源于硬件。早期各家研发的各类元器件都不通用也没有标准,相互使用很是困难,因而你们约定了功能和规格,就产生了接口,后来蔓延到软件中。编程

接口蔓延到软件以后,又分为 ABI(Application Binary Interface) 和 API(Application Programming Interface) 。
前者主要约定了二进制的运行和访问的规则,后者则 专一于逻辑模块的交互。本文如下内容仅讨论开发者常常接触的 API。swift

不少人对 API 的印象只是包含一些函数的 Class 或 头文件。但 API 在咱们生活中无处不在,只是咱们有时并无注意到。api

好比,当咱们在拨打电话时,手机和基站通讯的整个系统是很是复杂的。微信

图片描述

好在咱们不须要了解内部的细节,仅须要把 11 位的电话号码传给“电话系统”的接口就能够,而隐藏的国家区号(如+86)能够理解为接口的默认参数。
这个高度抽象的 API 背后,隐藏了很是多的细节。借助上面的中间层理论,咱们能够系统性地讨论设计一个 API 所须要考虑哪些内容。架构

图片描述

模块对上层暴露的 API 如何被使用?框架

API 从使用的耦合方式上,能够分为两类:一种是经过协议调用,如调用 HTTP 接口;另外一种是语言直接经过声明调用。
如设计 HTTP Restful API 时,并不须要关心使用者的操做系统、使用的编程语言、内存线程管理等,所以会比后者简单一些。

API 从使用者的规模和可控范围上,能够分为 LSUD(Larget Set of Unkown Developers) 和 SSKD(Small Set of Kown Developers) 两种。
前者通常都是公网开放的云服务,任何开发者均可以使用,没法提早预知以何种姿式被使用,版本也不可控制。融云提供的通讯云就是这种 API。
后者用户群有限,通常都在同一家公司或团队内。好比前段时间比较火的组件化,即对内提供的模块化 API,使用范围和方式都可控,在更新时通常不用太纠结向后兼容。

API 的第一受众是人,而后才是机器,因此“可理解性”在设计时须要优先考虑。
而良好的 API 文档、简单扼要的 Demo、关键的 log,能够提高 API 使用者的体验。

API 所属模块对下层有什么依赖?

API 所属模块都运行在必定的地址空间中。而其中的环境变量、加载库、内存和线程模型、系统和语言特性都须要考虑。

API 所属模块的内部实现对其余层有什么影响?

通常而言,设计良好的 API 在使用时,并不须要理解其内部实现。但若是能了解其内部架构并辅助关键 log,有助于提高使用 API 的效率。
而且模块的内部实现,有时也会影响到 API 设计的风格。
如一个强依赖 IO 的接口,可能须要使用异步的方式。大量异步的方式,就衍生出了 RxJava 等框架。

向后兼容
由于 API 如此重要,涉及的范围又如此普遍,广大开发者对 API 的向后兼容能够说要求很是高。
毕竟谁也不想在开发过程当中,频繁的更新接口和代码,想一想《 swift 从入门到精通到再次入门到再再次入门》的惨案就心有余悸。

咱们不只问,为何不少公司或者项目都没法向后兼容,仅仅是投入不够或不够重视,仍是说 100% 的向后兼容实际就是不可能的?

假设设计是理想和通过论证的,正如一个完美的圆圈。
设计是要落实到编码中的,而编码的过程当中老是不可避免的引入一些 bug,而带着 bug 的某个版本实现,其实正如一个 Amoeba 变形虫,形态是不固定的。而随着版本不断演进,不可避免会产生必定的差别。

第一个版本实现:

图片描述

第二个版本实现:

图片描述

因此说 100% 向后兼容自己就是不可能的。

所以,你们平时在谈论 API 稳定性时,其实默认是能够包含必定程度变动的。

但因为 API 涉及的范围太普遍,保障向后兼容都须要极大代价。
好比 Linux 就但愿快速迭代,彻底不保证 API 的稳定性。针对这个问题,Linux 还特地写了 stable-api-nonsense 文档。
有兴趣的能够点击阅读:stable-api-nonsense.rst

渐进式改进
因此说,保障 API 的稳定性会面临不少挑战,好比:

  • 业务形态还不稳定,还在高速发展
  • 业务和 API 历史包袱较重
  • 多个平台和语言的特性不一致
  • 用户群和使用方式不明确

咱们回顾一下正常的开发流程,看看是否能经过一些指标和工具,改善 API 的稳定性,主要涉及:需求、设计、编码、Review、测试、发布、反馈等步骤。

※需求

普通的产品开发,在启动的时候,用户需求都比较明确,但对于 LSUD 的云服务而言,没法提早预知用户群都有哪些,以及用户在他的产品中如何使用 API。
这容易形成,没有明确的用户需求,API 就很差进行设计和迭代,没有设计就没有用户,需求更无从谈起。这是一个鸡生蛋、蛋生鸡的问题。

建议能够在 API 发布以前,内部先针对典型的使用场景,设计几个完整的 Demo,验证 API 的设计和使用是否合理。
须要注意的是,Demo 须要有完整应用场景,达到上架地步,若是能内部使用, Eating your own dog food 最好,过于简单的 Demo 没法提早暴露 API 的使用问题。

Demo 的开发人员最好与 API 的设计者有所区分,避免思惟固化,更多内容你们能够参照 Rust 语言开发在自举过程当中的一些实践。

※设计

在设计 API 的时候,有不少须要注意的点和普通开发不太同样。

普通开发,快速实现功能始终被放在第一位。好比你们会用一些敏捷开发的方式,优先实现功能再快速迭代等。
但 API 设计时,接口没法频繁变动,因此首先须要考虑的是“少”,少便是多。

l 每一个 API 作的事情要少

一个接口只作一件事,把这个事情作好就足够了。
须要避免为了讨好某个场景,在一个 API 上进行复杂的组合逻辑,提供一个相似语法糖的接口。不然,场景的业务自身在演进时,很难保证 API 的行为不变。
若是须要支持多种业务,能够考虑将 API 分层,好比融云客户端的 API 会分为下面几层。

图片描述

举个例子,融云考虑通用性,基于订阅分发的模型,抽象了 RTCLib,客户端能处理媒体的任意流,很是的灵活,可是对于用户而言开发代价可能高些,要思考和作的工做比较多。
考虑到大量的用户,其实须要的是音视频通话的业务,基于 RTCLib,融云分装了不带 UI 的 CallLib 以及集成了 UI 的 CallKit。
若是一个用户,需求和微信的音视频通话相似,能够集成带 UI 界面的 CallKit,开发效率会很是高;
若是用户对通话音视频通话 UI 的交互有大量需求,能够基于 CallLib 进行开发,对 UI 能够进行各类定制。

l 暴露的信息要少

成熟的 API 设计者都会尽量的隐藏内部实现细节。
好比字段不该该直接暴露而是经过 Getter/Setter 提供,不须要的类、方法、字段都应该隐藏,都已经成为各个语言的基础要求,在此就不细述了。
但容易被忽略的一点须要提醒你们,应尽可能隐藏技术栈的信息。
好比:API http://api.example.com/cgi-bi...,就明显混入了不少无用的信息,而且之后技术切换升级想维持 API 稳定很是麻烦。

l 行为扩散要少

在语言直接调用的 API 中,须要避免基础接口经过继承致使行为扩散。
在普通的编码过程当中,抽象类和继承都是面向对象的强大武器。可是对于 API,更建议经过组合使用。
好比一个管理生命周期的类,若是被继承,子类有些行为就有可能被修改而致使出错。这时候建议使用 Interface + 工厂的方法提供实例。
因为 Java 8 以前 interface 没有 default 实现,为了不增长功能须要频繁修改接口,可使用 final class。
Objetive-C 则可使用 __attribute__((objc_subclassing_restricted)) 和 __attribute__((objc_requires_super) 控制子类继承行为。

l 画风切换要少

API 命名要作到多个平台的业务命名统一,与每一个平台的风格统一。
这点 HTTP 的接口要简单一些,只须要选定一种风格便可,Restful 或者 GraphQL 或者本身定义。
语言调用的 API 命名,建议首先遵循平台的风格,而后再是参考语言标准,最后才考虑团队的风格。
好比:iOS 平台的 API 开发,须要首先参照 iOS 的命名风格,did 和 will 之类的时态就很是有特点。
命名上细节较多,词汇、时态、单复数、介词、⼤小写、同步异步风格等都须要考量,须要长时间的积累。

l 理解成本要少

通常 API 每一个接口都会有相应的注释说明,可是值得注意的是,大部分开发者并不看注释。
大部分开发者对接口的了解,都仅源于 IDE 的补全和提醒。一个接口看着像就直接用,不行再换一个试试,这实际上是一种经验式编程的方式。
也就意味着接口命名须要提升可理解性。有一个办法能够验证,将接口的全部注释抹掉,使用者可否很是直接的看懂每一个接口的含义。若是很困难,则须要改进。

API 设计还有一处和普通开发不太一致。普通开发设计好架构便可,每一个模块的开发多是同一我的,接口并不须要在设计时肯定下来。
可是 API 的设计阶段,须要进行 Review 并直接肯定接口的设计,以保证多端在开发时遵循彻底一直的规则。

※编码

在 API 的编码过程当中,有如下几点须要注意。

在 API 中,预约义好版本号。
这个主要是针对 HTTP API,如:http://api.example.com/v1/use...。 若是目前仅有一个版本,也能够暂时不加,第二版时再区分。

注意 API 版本检查。
当分层提供多种 API 时,每层 API 须要在启动时,先校验一下版本号,避免不匹配的状况。
好比在如下 Java 代码中,你们可能以为判断版本号相等的代码很是奇怪,应该永远是 true 才对。

图片描述

可是抽象类和实现类出如今不一样的分层模块中,而且实现类先编译,抽象类版本更新后再编译,就会出现不一致的状况。有不少语言或平台能提供相似的方式来肯定版本。

提供规范性的 log 输出。
普通开发的log,主要用于本身定位问题。可是 API 在编码时,最好针对性的添加一些 log,有利于 API 的使用者理解并简单排查问题。
但出于性能考虑,须要定义好 log 的级别并能够调整。

注意废弃与迁移。
当一个之前设计的 API 再也不符合要求或者有重大问题时,咱们能够对外标记成已废弃,并在注释中建议使用者迁移到另外一个接口。
若是是相似的被废弃接口,内部编码时最好能使用新的接口来实现,以下降向后兼容的维护成本。
HTTP 的 API,须要预约义好迁移的错误码,好比在 HTTP 规范中,可使用 410 Gone 说明已经再也不支持某个接口。

※Review

API 的 Review 基于普通开发的 Code Review。
若是基础的 Code Review 都没有作好,确定没法保障 API 的质量和稳定性。

能够经过一些工具,为 API 的 Review 提供一些参考报告。
好比可使用 SonarLint 分析代码复杂度,若是接口层的代码复杂度较高,会是一个危险的信号。
还能够借助 Java 反射、Clang 语法分析,获取当前的 API 接口列表,生成接口变动报告,也有利于减小无用接口的暴露。
另外,自动化工具生成的接口文档也是 Review 重要的一环。

※测试

在测试环节,咱们能够经过 unit test 来关注 API 的稳定性。
与敏捷开发常常修改 test case 不一样,API 的 test case 基本表明了接口的稳定性。因此在修改旧 case 时须要特别明确,是 case 自身的 bug 仍是接口行为发生了变动。

※发布

咱们能够经过区分 dev 和 stable 版本,为不一样阶段的开发者提供更好的体验。

dev 版本包含最新的功能,可是 API 接口有变动风险。stable 版本 API 稳定,但功能不必定是最新的。
若是开发者还在开发过程当中,能够选用最新的 dev 版本,基于最新 API 开发。
若是应用已经上线,能够选择升级直接到最新的 stable 版本。

※反馈

因为前面提到的,云服务的 API 比较难肯定用户群和用户的使用方式。
能够参考 APM(Application Performance Management) 的方式,记录热点 API 使用状况,为后续的优化提供数据。

总结
上面的改进,让保障 API 的稳定性变得更容易。
下面以融云 IMLib iOS SDK 2.0 版本演进为例,历尽 2015至 2019 四年时间,从 2.2.5 到 2.9.16 共 98 个版本。
API 接口数量翻了一番,考虑到接口更内聚,功能大约增长了 3 倍。

图片描述

可是须要用户迁移的接口很是少,即便迁移时开发成本都很是低。

图片描述

图片描述

更多干货内容请点击注册查看!

相关文章
相关标签/搜索