如何设计一个良好的 API 接口?

API 是软件系统的核心,而我们在设计 API 接口的同时,面临着非常多的挑战,从遇到的场景上来看,它是多样的,那么,如何设计一个处处适用的 API 呢?我们所参与的业务不断演进,那么,又如何设计一个有兼容性的 API 呢?我们的软件流程是协同开发的,那么,我们如何实现对 API 的统一认知呢?今天我想大家探讨一下,如何设计一个良好的 API 接口。

好的 API 设计

什么样的API设计是好的设计?弄明白了这一点,这将帮助我们更好地进行 API 接口设计。好的 API 设计,需要同时考虑到这样几个要素:标准化、兼容性、抽象性、简单性和高性能。可以说,这几个要素缺一不可。

good-api
好的API

API 标准化

对于 Web API 标准化而言,一个非常好的案例就是RESTful API,目前业界的 Open API 多数是基于 RESTful API 规范设计的,需要注意的是RESTful API 具有成熟度模型,如下图(摘自 Richardson Maturity Model)示:

glory-of-rest
Glory of REST

其中 Level0 是普通的请示响应模式。Level1 引入了资源的概念,各个资源可以单独创建 URI。与Level0 相比,它通过资源分而治之的方法来处理复杂的问题。Level2 引入了一套标准的 HTTP 协议,它通过遵守 HTTP 协议的动词,并配合 HTTP 响应状态码来规范化 Web API 的标准。Level3 中使用超媒体,可以使协议拥有自我描述的能力。通常情况下,成熟度模型中达到了 Level2,就已经非常好了。

在 RESTful API 中,每一个 URI 代表一种资源,这里 URI 是每一个资源地址的唯一资源定位符。所谓资源,实际上就是一种信息实体。它可以是服务器上的一段文本、一个文件、一张图片、一首歌曲或者是一种服务。RESTful API 规定了,通过 GETPOSTPUTPATCHDELETE 等方式,对服务端的资源进行操作,因此,在定义一个 Web API 的时候,需要明确定义出它的请求方式、版本、资源名称和资源 ID,如下图示:

api-uri
RESTful  API URI

举个例子来说,要查看用户编码是 1001 的用户信息,我们可以定义GET的请示方式,它的版本是 v1,资源名称是 users,资源  ID 是 1001,即:[GET] /v1/users/1001

这里思考一下,如果存在多个资源组合的情况呢?事实上,我还可以引入子资源的概念,需要明确定义出它的请求方式、版本、主资源名称与主资源 ID,以及子资源名称与子资源 ID,如下图示:

api-uri-mul
coRESTful API URI 多资源

继续举个例子来说,要查看用户编码是 1001 用户的权限信息,我们可以定义GET的请求方式,它的版本是 v1,主资源名称是 users,主资源  ID 是 1001,子资源名称是 roles,子资源  ID是 101,即:[GET] /v1/users/1001/roles/101

有时候当一个资源变化,难以使用标准的  RESTful API 来命名时,就可以考虑使用一些特殊的 actions 命名,比如密码修改接口,可以定义PUT的请示方式,它的版本是 v1,主资源名称是 users,主资源  ID 是 1001,资源字段是 password,然后定义一个 actions 的操作是modify,即:[PUT] /v1/users/1001/password/actions/modify

与此同时,建议不要试图创建自己的错误码和返回错误机制。很多时候,我们觉得提供更多的自定义的错误码,有助于传递信息,但其实,如果只是传递信息的话,错误信息字段可以达到同样的效果。此外,对于客户端来说,很难关注到那么多错误的细节,这样的设计只会让 API 变得更加复杂和难于理解。因此,建议遵守RESTful API 的规范,使用 HTTP 规范的错误码,例如:

状态码描述
200请求成功
400错误的请示
401未验证
403无权限
404无法找到
409 资源冲突
500服务器内部错误

当非200的HTTP错误码响应时,可以采用全局的异常结构响应信息,这里列出最为常用的几个字段:

HTTP/1.1 400 Bad Request 
Content-Tpye: application/json 
{ 
   "code":"INVALID_ARGUMENT", 
   "message":"{error message}", 
   "cause":"{cause message}", 
   "request_id":"01234567-89ab-cdef-0123-456789abcdef", 
   "host_id":"{server identity}", 
   "server_time":"2020-4-10T14:20:22Z"
}

其中code字段用来表示某类错误的错误码,例如上述列表的非 200 状态码,而message字段用来表示错误的摘要信息,它的作用是让开发人员能快速识别错误,server_time字段用来记录发送错误时的服务器时间,它可以明确地告诉开发人员发生错误时的具体时间,便于在日志系统中,可以根据时间范围搜索,来快速定位错误信息。

除此之外 ,响应的内容会根据不同的情况,做出不的响应。如果是单条数据,返回一个对象的 JSON 字符串;如果是列表数据,则返回一个封装的结构体。如下响应内容:

{ 
   "count":100, 
   "items":[{},... ], 
}

涵盖count字段和items字段,count字段表示返回数据的总数据量,这里要注意,如果接口没有分页的需求,尽量不要返回这个count字段,因为查询总数据量是耗性能的操作。此外,items字段表示返回数据列表,它是一个 JSON 对象数组。

综上所述,规范就是大家约定俗成的标准,如果都遵守这套标准,自然沟通成本也就大大降低了。

API 兼容性

由于我们参与的业务是不断演进的,设计一个有兼容性的 API 就显得尤为重要了,如果接口不能够向下兼容,业务就会受到很大影响。例如,我们的产品是涵盖  Android端、iOS端和PC端的,都运行在用户的机器上,用户必须升级产品到最新的版本,才能够更好的使用。同时,我们还可能遇到服务端不停机升级,由于 API 不兼容,而导致短暂的服务故障。因此,为了实现 API 的兼容性,引入了版本的概念,如上述的案例中,通过 URL 保留版本号,实现了兼容多个版本。

API 抽象性

通常情况下,我们的接口抽象都是基于业务需求的,因此,我们一方面要定义出清晰的业务问题域模型,例如数据模型和领域模型等。并建立起某个问题的现实映射,有利于拉起不同的角色对 API 设计认知上的统一。另一方面,API 设计如果可以实现抽象,就可以很好地屏蔽具体的业务实现细节,为我们提供更好的可扩展性。

API 简单性

要想更进一步理解它,就一定要知道它的主要宗旨是什么——即需要遵守“最少知识原则”。怎么来理解“最少知识原则”呢?其实,它就是客户端,不需要知道那么多服务的 API 接口,以及这些 API 接口的调用细节,比如设计模式的外观模式和中介者模式,都是它的应用案例。

api-simple
API 简单性

举个例子(如上图示),外观接口将多个服务进行业务封装与整合,调用给客户端使用,这样设计的好处是什么呢?我们可以一目了然,客户端只需要调用这个外观接口就行了,省去了一些繁杂的步骤。

API 高性能

最后我们还要关注性能,外观接口虽然保证了简单性,但是增加了服务端的业务复杂度,同时,由于多服务之间的聚合,导致它们的接口性能也不是太好,此外,我们还需要考虑入参字段的各种组合,会不会导致数据库的性能问题,有时候,我们可能暴露了太多字段给外部组合使用,导致数据库没有相应的索引,而发生全表扫描,事实上,这种情况在查询的场景下,非常常见。因此,我们可以只提供存在索引的字段组合给外部调用。

关于如何设计好的 API 接口到这里就结束了,本文主要是学习笔记,学习内容来源于极客时间每日一课《如何设计一个良好的 API 接口?》,大家有兴趣可直接看原文,最后,留下一个问题用于讨论交流:如何面对接口设计中的不确定性呢?

《如何设计一个良好的 API 接口?》的相关评论

发表评论

必填项已用 * 标记,邮箱地址不会被公开。