Design RESTfulAPI —— 从领域建模到 API
REST简介
RESTful,是目前非常流行的一种互联网软件架构,由于其结构清晰、易于理解、便于扩展,正得到越来越多开发者的青睐。
什么是REST
REST(REpresentational State Transfer),首次出现是在2000年Roy Thomas Fielding的博士论文中,它值得是一组架构约束条件和原则。满足这些约束条件和原则的应用程序和设计就是RSETful的。为了理解什么是REST,我们首先需要理解下面几个概念。
- 资源(Resource)
REST是“表现层状态转化”,其中暗含的主语便是资源,即资源的表现层状态转化。那么在REST的定义中,什么是资源呢?其实很好理解,我们平时上网所看到的一篇文章、一首歌曲、一个视频等,都可以算作资源。这些资源都可以通过URI来定位,即一个URI表示一个资源。 - 表现层(REpresentation)
一个资源,也就是一个信息实体,它可以有多重不同的表现形式。例如:文本可以用txt格式表现,也可以用json或xml的格式表现,这就是表现层的意思。
通过URI可以确定一个资源,但是如何确定资源的表现形式呢?应该通过HTTP请求头的Accept和Content-Type字段指定。因此,严格来说,如果我们采取了RESTful架构,那么URI中是不应该有.jsp或者.html的后缀名的。因为URI只是用于定位资源,而不负责资源的表现形式。 - 状态转化(State Transfer)
访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,肯定涉及到数据和状态的变化。而HTTP协议是无状态的,那么这些状态肯定保存在服务器端,所以如果客户端想要通知服务器端改变数据和状态的变化,肯定要通过某种方式来通知它。
客户端能通知服务器端的手段,只能是HTTP协议。具体来说,就是HTTP协议里面表示操作方式的动词,如:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。
综合上面的解释,我们可以对RESTful架构做一个简单总结:
- 每一个URI代表一种资源
- 客户端和服务器之间,资源以某种形式进行传递,即传递资源的表现层
- 客户端通过不同的HTTP动词(POST、PUT/PATCH、GET、DELETE、HEAD、OPTIONS)对资源进行操作,从而达到“表现层状态转化”的目的。
REST成熟度模型
在Roy Thomas Fielding的论文中,对REST架构风格做了以下几点约束。
- Client-Server 客服端-服务器模式
- Stateless 无状态
- Cacheable 可缓存
- Layered System 分层系统
- Code on Demand 按需扩展
- Uniform interface 统一接口
严格来说,符合所有这些约束的服务才能被称为 RESTful,但是在实际开发中,这需要付出较为高昂的成本,可能我们常见的服务都不是完全 RESTful 的,那么如何去判断一个服务有多么 RESTful 呢?Richardson 提出的 REST 成熟度模型可以帮助我们分析。模型对于REST服务的成熟划分了如下四个等级。
-
第0级
只使用一个 URI 作为服务端口,使用 HTTP 协议传输数据。SOAP 和大多数 WS-* 服务都是属于这个级别的,XML-RPC 和 POX 也是。这种做法相当于把 HTTP 这个应用层协议降级为传输层协议,HTTP 头用来保证传输,而所有的业务逻辑都包含于有效载荷中。 -
第1级
使用多个 URI,不同的 URI 代表不同的调用入口,但只使用同一个 HTTP 方法传输数据。目前与第0级的区别在于:第0级相当于传入参数的远程函数调用,而目前需要首先获取相应资源再调用相应方法,可以类比于面向对象中首先获取对象标识符然后调用相应方法。 -
第2级
使用多个 URI,不同的 URI 代表不同的资源,同时使用多个 HTTP 方法操作这些资源,例如使用POST/GET/PUT/DELETE 分别进行 CRUD 操作,这时候 HTTP 头和有效载荷都包含业务逻辑,例如 HTTP 动词对应相应操作,HTTP 状态码对应操作结果的状态。目前与前两级服务的区别在于:相比于只使用 POST 的第0级和第1级服务,更为合理地使用了所有 HTTP 动词。
-
第3级
最后这个级别引入了HATEOAS(Hypertext As The Engine Of Application State),超媒体控制(Hypermedia Control)的关键在于它告诉我们下一步能做什么,以及对应的资源URI。目前与第2等级的区别在于:客户端在知道服务端入口的情况下,不再需要任何服务端的先验知识,可以根据服务端的返回完成与服务端的交互。
API设计方法原则
-
API设计接轨业务流程的详细分析与设计
对客户而言,API如同一本说明书,告诉客户有哪些功能可用,以及使用的具体方式;而对服务提供团队而言,API体现为团队对外的责任。因此,只有立足于对业务流程与客户需求的详细分析,才能通过API封装复杂的业务逻辑,从而明确团队需要对外提供的服务。
-
团队统一建模方法,统一技术语言
业务流程的分析可以帮助我们建立业务模型,从而借助模型进一步分析核心业务数据的流动,以便于后续通过模型和数据流动设计合理的 API。业务模型的设计接轨于业务流程的分析,同时也是 API 设计的前提。不仅如此,在团队中,业务模型可以作为不同角色的团队成员交流的工具:例如,Devloper 可以通过模型与 BA 确认业务需求。统一的建模方法,可以最大程度保证各种角色的团队成员对于业务的理解是一致的。从业务模型中,我们可以抽象出 REST 语义下的资源,资源间的关系可以通过业务数据的流向分析。数据的流动会造成资源状态的变化,而我们要进一步抽象的API便可以表征资源状态的变化过程。
-
结合业务含义正确使用HTTP动词
在REST语义下,HTTP各动词含义如下表所示:
方法 描述 幂等(Y/N) POST 根据客户端提供的数据创建一个资源 N GET 返回资源的当前展现 Y PUT 根据客户端提供的数据替换指定资源,或者创建一个新的资源 Y DELETE 删除某个资源 Y HEAD 返回资源的元信息(如Last-Modified等) Y PATCH 部分更新资源 N OPTIONS 获取当前资源信息,比如当前资源支持哪些方法的信息 Y 根据RFC-2616,幂等是指一次和多次请求某一个资源应该具有相同的副作用。幂等的方法意味着请求成功执行所得到的结果不依赖于该方法被执行的次数。例如:通过 PUT 方法将某个资源的 satus 属性置为success,那么无论是第一次执行还是多次执行,获得的结果都是相同的,即执行完成之后都是 status=success。
API设计详细步骤
梳理业务流程
业务流程的梳理,是我们工作的起点。Event Storming是一种有效的帮助我们梳理业务的方法,Event Storming的具体步骤可以参考这篇文档,本文不再赘述。
建立系统模型
完成业务模型的梳理之后,下一步就是建立系统业务模型了,笔者采用的方法是四色建模,InfoQ上的这篇文章已经做了详细介绍。简单总结如下:
- 以满足业务运行为前提,寻找需要追溯的事件。
- 根据需要追溯的事件,寻找事件相关的时标性对象。所谓时标性对象,一般与时间有紧密相关关系。
- 寻找时标对象周围的人、事、物,并分析它们之间的关系。
- 从人、事、物中抽象角色。
- 用描述性对象补充说明系统中的人、事、物。
那么,我们如何判断我们通过上述方法建立的模型是可用的呢?答案便在于业务数据流。我们可以在建立的业务系统模型上,模拟业务事件的发生,分析业务数据流是否能够在模型上流通,从而判断我们建立的模型能否支撑我们的业务。
以笔者参与的一个实际项目为例:某公司技术人员管理许多数字资产,业务部门的人员需要通过一定的策略对数字资产的状态进行检查,并产生对应的报表。业务建模过程如下:
- 寻找需要追溯的时标事件
- 寻找时标对象周围的人/事/物
-
从中抽象角色
role-of-object.png -
把一些信息用描述对象补足
description-of-object.png -
寻找模型中的聚合、引用等关联关系
- 识别聚合根
识别资源及设计URI
根据业务模型,我们分析其中的资源和聚合根,并以每个聚合根为起点,开始设计 URI。在上面的例子中,我们的聚合根为 User 和 policy,因此URI设计如下表。
编号 | URI |
---|---|
1 | /users |
2 | /users/{uid} |
3 | /users/{uid}/tasks |
4 | /users/{uid}/tasks/{tid} |
5 | /users/{uid}/tasks/{tid}/reports |
6 | /users/{uid}/tasks/{tid}/reports/{rid} |
7 | /users/{uid}/assets |
8 | /users/{uid}/assets/{aid} |
9 | /policies |
10 | /policies/{pid} |
结合HTTP动词定义RESTful API
结合第一部分说明的在 REST 语义下各 HTTP 动词的含义,分析每个 URI (资源)在这些动词下是否有业务含义,分析如下表。
POST | GET | DELETE | PATCH | PUT | HEAD | OPTIONS | |
---|---|---|---|---|---|---|---|
1 | √ | √ | |||||
2 | √ | √ | |||||
3 | √ | √ | |||||
4 | √ | √ | |||||
5 | √ | √ | |||||
6 | √ | ||||||
7 | √ | √ | √ | ||||
8 | √ | √ | |||||
9 | √ | √ | √ | ||||
10 | √ | √ | √ |
结合具体的业务场景和操作者,可以分析每个 API 应该返回的状态码。下表给出了部分(有关 user 和 asset ) API 的使用场景与返回状态码的结果。
WHO | URI | METHOD | SCENE | RESPONSE |
---|---|---|---|---|
Any | /users | POST | 创建用户 | 成功创建201 |
Any | /users | POST | 创建用户 | 信息有误400 |
Admin | /users | GET | 查询所有用户信息 | 成功获取200 |
!Admin | /users | GET | 查询所有用户信息 | 非法获取403 |
Admin or Uid | /users/{uid} | GET | 查询某个用户信息 | 成功获取200 |
Admin or Uid | /users/{uid} | GET | 查询某个用户信息 | 用户不存在404 |
!{Admin or Uid} | /users/{uid} | GET | 查询某个用户信息 | 非法获取403 |
Uid | /users/{uid} | PATCH | 修改某用户个人信息 | 成功修改204 |
Uid | /users/{uid} | PATCH | 修改某用户个人信息 | 修改有误400 |
!Uid | /users/{uid} | PATCH | 修改某用户个人信息 | 非法修改403 |
Admin or Uid | /users/{uid}/assets | POST | 创建资产 | 成功创建201 |
Admin or Uid | /users/{uid}/assets | POST | 创建资产 | 信息有误400 |
!{Admin or Uid} | /users/{uid}/assets | POST | 创建资产 | 非法创建403 |
Admin or Uid | /users/{uid}/assets | GET | 查询某用户所有资产信息 | 成功获取200 |
!{Admin or Uid} | /users/{uid}/assets | GET | 查询某用户所有资产信息 | 非法获取403 |
Admin or Uid | /users/{uid}/assets | DELETE | 批量删除某用户的某些资产 | 成功删除204 |
Admin or Uid | /users/{uid}/assets | DELETE | 批量删除某用户的某些资产 | 信息有误400 |
!{Admin or Uid} | /users/{uid}/assets | DELETE | 批量删除某用户的某些资产 | 非法删除403 |
Admin or Uid | /users/{uid}/assets/{aid} | GET | 查询某资产详情 | 成功获取200 |
Admin or Uid | /users/{uid}/assets/{aid} | GET | 查询某资产详情 | 找不到资产信息404 |
!{Admin or Uid} | /users/{uid}/assets/{aid} | GET | 查询某资产详情 | 非法获取403 |
Admin or Uid | /users/{uid}/assets/{aid} | PATCH | 修改某资产信息 | 成功修改204 |
Admin or Uid | /users/{uid}/assets/{aid} | PATCH | 修改某资产信息 | 修改有误400 |
Admin or Uid | /users/{uid}/assets/{aid} | PATCH | 修改某资产信息 | 找不到资产信息404 |
!{Admin or Uid} | /users/{uid}/assets/{aid} | PATCH | 修改某资产信息 | 非法修改403 |
分析资源关系实现HATEOAS
通过上面的分析,我们得到了每个 API 的返回 Http 状态码,而对于其返回体的设计,可以根据资源及其相关资源的关系来确定。例如 user 和 asset 有关联关系,那么对于GET /users/{uid}
而言,示例返回的 body 体如下:
{
"data": {
"url": "/users/1",
"attributes": {
"id": 1,
"name": "felix",
"email": "felix@example.com"
},
"relationships": {
"assets": {
"url": "/users/1/assets"
}
}
}
}
用户通过该 API 可以得到用户的详细信息,而通过返回体可以拿到获取该用户下 assets 的API 而无需事先知道这个 API 的存在。理想情况下,资源与资源见的关系组成一张有向关系图,用户从任何一个资源入手,均可以遍历他有权限访问的所有资源。
自动化Doc (Swagger)
设计好的 RESTful API 可以借助 Swagger 形成文档,从而指导开发。最新的 Swagger 支持 Open API 3.0 规范,文档更新方便,并且支持文档的执行,具备较为完善的生态圈,推荐用其发布我们的 API 文档。