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设计规范并严格遵守。