2.3.2 设计API

服务拆分完成后,下一步就是将系统的操作映射到服务中,即设计API。如果服务之间需要协助才能完成业务流程,且需要定义协作API,我们一般称这种API为内部API。设计API通常有三个步骤:确定要操作的资源、确定资源的操作方法、定义具体细节,下面我们具体介绍。

1.确定要操作的资源

资源是业务系统中的某种数据模型。操作资源的API通常都是RESTful API,它是HTTP/JSON格式下的事实标准,我们团队对外暴露的OpenAPI和前端调用的接口都遵循这种风格。REST是以资源为核心的一种API设计风格,客户端通过统一资源标识符(URI)来访问和操作网络资源,通过一组方法确定对资源的操作方式,比如获取、创建等。

在使用HTTP作为传输协议时,资源名称会被映射到网址,方法被映射到HTTP的方法名。对于服务间相互调用的内部接口,我们遵循gRPC标准格式进行定义。本质上,gRPC API的定义风格和REST风格非常类似,也将资源和方法两部分组合成接口名。很多时候,资源和我们定义的领域模型有一一对应的关系,但在有的情况下,我们可能需要设计拆分或聚合的API,这要结合具体需求进行分析。

资源是业务实体,必须有一个资源名称作为唯一的标识符,一般由资源自身的ID及父资源ID等构成。举一个最简单的例子,我们要为用户管理模块定义API,核心资源就是用户。获取资源的URI一般都以复数形式表示,也就是所谓的集合。集合也是一种资源,指一个同类型的资源列表。比如用户集合可以定义为users。下面的例子描述了如何定义一个获取用户资源的URI。

如果资源中包含子资源,那么子资源名称的表示方法是在父资源后面接子资源。下面的例子定义了获取用户地址的URI,地址是用户的子资源,一个用户会有多个地址。

2.确定资源的操作方法

方法是对资源的一种操作。绝大部分资源都具有我们常说的增删改查的方法,这些是标准方法。如果这几个方法不能代表系统的行为,我们还可以自定义方法。

(1)标准方法

标准方法一共有五个,分别是获取(Get)、获取列表(List)、创建(Create)、更新(Update)和删除(Delete)。

● 获取(Get)

获取方法以资源ID为入参,返回对应的资源。下面的例子展示了获取订单信息API的实现过程。

● 获取列表(List)

列表将集合名称作为入参,返回与输入相匹配的资源集合,即查询相同类型的数据列表。获取列表和批量获取不太一样,批量获取要查询的数据不一定属于同一个集合,因此入参要设计为多个资源ID,返回结果是这些ID对应的资源列表。另外需要注意的一点是,列表API通常应该实现分页功能,避免返回过大的数据集给服务带来压力。列表的另外一个常用的功能是给返回结果排序。下面是一个列表API的Protobuf定义过程,对应的RESTful API定义在get字段中。

● 创建(Create)

创建方法需要以资源的必要数据作为请求体,以HTTP POST方法发送请求,并返回新建的资源。有一种设计是只返回资源ID的,但笔者建议返回完整数据,这可以帮助获取入参中没有发送的由后端自动生成的字段数据,避免后续再次查询,API的语义也更加规范。还需要注意的是,如果请求入参可以包含资源ID,意味着该资源对应的存储被设计为“可以写入ID”而不是“自动生成ID”。另外,如果因为某个具有唯一性的字段重复导致创建失败,比如资源名称已存在,那么API的错误信息中应该明确告知。下面的例子展示了创建订单API的实现过程,与获取列表不同的是,HTTP方法为POST且具有请求体。

● 更新(Update)

更新和创建比较类似,只不过需要在入参中明确定义要修改资源的ID,返回结果为更新后的资源。更新对应的HTTP方法有两种,如果是部分更新,使用PATCH方法,如果是完整更新,使用PUT方法。笔者不建议完整更新,因为添加新资源字段后会出现不兼容的问题。另外,如果因为资源ID不存在而导致更新失败,应明确返回错误。下面的示例展示了更新订单API的实现过程,它和创建API非常相似。

● 删除(Delete)

删除方法以资源ID为入参,使用HTTP的DELETE方法,返回内容一般为空。但如果仅仅是将资源标记为已删除,实际数据还存在,则应返回资源数据。删除应该是一个幂等操作,即多次删除和一次删除没有区别。后续的无效删除最好返回资源未发现的错误,避免重复发送无意义的请求。下面的示例展示了删除订单API的实现过程。

表2-1描述了标准方法和HTTP方法之间的映射关系。

表2-1

(2)自定义方法

如果上面介绍的标准方法不能表达你要设计的功能,可以自定义方法。自定义方法可以操作资源或集合,对请求和返回值也没有太多要求。一般情况下资源是确定的,所谓自定义只是定义操作而已,比如ExecJob代表执行某个任务。自定义方法一般使用HTTP的POST方法,因为它最通用,入参信息放在请求体里。查询类型操作可以使用GET方法。对URL的设计有所不同,一般建议使用“资源:操作”这样的格式,示例如下。

不使用斜杠的原因是,这样有可能破坏REST的语义,或者与其他URL产生冲突,所以建议使用冒号或者HTTP支持的字符进行分割。以下代码为自定义的取消订单操作的API。

3.定义具体细节

API签名定义好以后,就可以基于业务需求定义具体细节了,包括请求入参、返回资源的数据项及对应的类型。对Protobuf来说,请求和响应都会被定义为message对象。我们继续使用上面的例子,分别为获取订单和创建订单API定义请求消息。需要注意的是,如果后续需要为消息添加字段,原有字段后的编号是不能改变的。因为gRPC协议的数据传输格式为二进制,编号代表具体的位置,修改后会导致数据解析错误。新字段使用递增的编号即可。

与之对应的,如果接口需要对外暴露为OpenAPI,则按照HTTP的要求定义好入参和返回值即可,请求体和响应体一般使用JSON格式数据。

最后再介绍几点设计中的注意事项。首先是命名规则,为了使API更易于被理解和使用,命名时一般遵循简单、直观、一致的原则。笔者列举了几点建议供参考。

● 使用正确的英语单词。

● 常见的术语用缩写形式,如HTTP。

● 保持定义的一致,相同的操作或资源使用相同的名字,避免出现二义性。

● 避免与开发语言中的关键字出现冲突。

在错误处理方面,应该使用不同的响应状态码来标识错误。有一种设计方法是为所有的请求都返回正常的200状态码,并在返回值中将error字段定义为true或false来区分请求的成功与失败,这种方法并不可取,除非有特殊的理由一定要这么做。业务错误时需要明确告知调用方是什么错误,即返回业务错误消息。我们的实践经验是,业务错误返回500状态码,并在error-message字段中说明错误原因。

总之,想要定义出易读、易用的API不是一件容易的事,除上面提到的内容外,在API文档和注释、版本控制、兼容性等方面都需要注意。笔者建议团队基于自身情况定义出完整的API设计规范并严格遵守。