周志明架构课-架构师视角

10.RESTful服务:如何评价服务是否RESTful

view

Richardson成熟度模型

  1. 第0级 The Swamp of Plain Old XML,完成不REST
  2. Resource,开始引入资源的概念
  3. HTTP Verbs,引入统一接口,映射到HTTP协议方法上
  4. Hypermedia Controls,超文本驱动

场景是这样的:
以开发一个医生预约系统为例,让病人可以通过这个系统,知道自己想要预约的医生在指定日期是否有空闲时间,以方便就诊

第0级成熟度: The Swamp of Plain Old XML
医院开放一个/appointmentService的web api。传入日期、医生姓名作为参数,就可以得到该时间段内,该医生的空闲时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//请求
POST /appointmentService?action=query HTTP/1.1

{date: "2020-03-04", doctor: "mjones"}

//响应
HTTP/1.1 200 OK

[
{start:"14:00", end: "14:50", doctor: "mjones"},
{start:"16:00", end: "16:50", doctor: "mjones"}
]

//请求
POST /appointmentService?action=comfirm HTTP/1.1

{
appointment: {date: "2020-03-04", start:"14:00", doctor: "mjones"},
patient: {name: xx, age: 30, ……}
}

//响应

HTTP/1.1 200 OK

{
code: 0,
message: "Successful confirmation of appointment"
}

这是基于RPC风格的服务设计,很好的解决了这个问题

第1级成熟度:Resources
围绕资源而非过程来设计服务,Endpoint应该是一个名词而不是动词,此外每次请求都应该包含资源ID,所有操作均通过资源ID来进行。上述的场景中,<font color="red">应该把什么看成是一种资源呢?医生?不是吧,应该是把医生的档期视作为一种资源</font>。具体过程,可以观察一下请求和响应的URI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//请求医生档期
POST /doctors/mjones HTTP/1.1

{date: "2020-03-04"}

//响应信息
HTTP/1.1 200 OK

[
{id: 1234, start:"14:00", end: "14:50", doctor: "mjones"},
{id: 5678, start:"16:00", end: "16:50", doctor: "mjones"}
]

//预约信息


POST /schedules/1234 HTTP/1.1

{name: xx, age: 30, ……}

这些个交互看起来解决了一些问题,而且抽象程度也第0级高了不少。但是还有一些问题需要解决

  1. 只处理了查询和预约,没有提供调整或者取消预约的接口
  2. 处理结果响应时,只能靠结果中的code、message信息做判断,不通用
  3. 没有考虑到认证授权等安全方面的内容
第2级成熟度,HTTP Verbs
通过统一接口来解决第1成熟度的三个问题,统一接口是什么呢?就是HTTP协议的标准方法。这些方法都是经过精心设计的,几乎可以涵盖资源可能会遇到的所有操作场景。
具体要如何理解呢?
1.针对预约变更的问题,把不同的业务需求抽象为对资源的增加、修改、删除等操作来解决
2.针对响应码的问题,可以直接使用HTTP协议的Status Code,这就可以涵盖大多数据资源操作可能会出现的异常问题
3.安全性问题嘛,可以依靠着Http Header中携带的额外认证、授权信息来解决

反应到代码请求中是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//请求
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
//响应

HTTP/1.1 200 OK

[
{id: 1234, start:"14:00", end: "14:50", doctor: "mjones"},
{id: 5678, start:"16:00", end: "16:50", doctor: "mjones"}
]

//预约

POST /schedules/1234 HTTP/1.1

{name: xx, age: 30, ……}

//预约成功

HTTP/1.1 201 Created

Successful confirmation of appointment

第3级成熟度:Hypermedia Controls
大多数系统都能够达到第2级成熟度,但是这种还是有点问题,就是前后不连贯,也就是我知道医生的空闲时间以后,怎么知道预约的地址呢?当然这是浏览器的角度,咱们大多都是依靠在程序硬编码来实现的。这是我们习以为常的操作,但REST的目的并不是这样的,它想的是:<strong>除了第一请求是由你在浏览器地址栏中输入驱动的以外,其它所有后续请求都能够自己描述清楚后续可能发生的状态转移,由超文本自身来驱动。</strong>唉多美好的愿望呀
它的表现形式是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//请求

GET /doctors/mjones/schedule?date=2020-03-04&statu s=open HTTP/1.1

//响应

HTTP/1.1 200 OK

{
schedules:[
{
id: 1234, start:"14:00", end: "14:50", doctor: "mjones",
links: [
{rel: "comfirm schedule", href: "/schedules/1234"}
]
},
{
id: 5678, start:"16:00", end: "16:50", doctor: "mjones",
links: [
{rel: "comfirm schedule", href: "/schedules/5678"}
]
}
],
links: [
{rel: "doctor info", href: "/doctors/mjones/info"}
]
}

这样的话,服务端的API和客户端的请求就能够做到完全解耦了,这个时候你再想调整什么东西是不是就很easy了

REST的不足之处与争议

1.面向资源的编程思想只适合做CRUD,只有面向过程、面向对象编程才能处理真正复杂的业务逻辑

其实,面向资源的编程思想与面向过程、面向对象编程思想,只是抽象问题时所处的立场不同,只有选择问题,没有高下之分:

  • 面向过程编程时,为什么要以算法和处理过程为中心,输入数据,输出结果?当然是为符合计算机中主流的交互方式。
  • 面向对象编程时,为什么要将数据和行为统一一起来、封闭成对象?当然是为符合现实民办的主流交互方式。
  • 面向资源编程时,为什么要将数据作为抽象的主体,把行为看作是统一的接口,当然为是符合网络世界的主流交互方式。
2.REST与HTTP完全绑定,不适用于要求高性能传输的场景中

REST的确依赖着HTTP协议的标准方法,状态码和协议头等,但我们要明确一个前提,HTTP协议是应用层协议,而不是传输层协议,如果在应用层协议追求传输效率,有点买椟还珠的意思。

3.REST不利于事务支持

对事务的理解不一一致的话,在这块歧义也会比较大。
刚性ACID事务,
分布式事务。
最终一致性事务

4.REST没有传输可靠性支持

REST的可靠性需要很多其他策略的配合,比如重试机制、幂等机制

5.REST缺乏对资源进行“部分”和“批量”的处理能力

这点还是挺有道理的,这可能也是未来面向资源思想和API设计风格发展的方向。
暂时的处理办法是:

  • 针对部分的应对手段是,通过位于中间节点或客户端缓存来缓解
  • 针对整体,只能是再抽象出一些资源来应对。GraphQL就是一种解决批量操作类问题的解决方案,它是一种有协议地、更彻底地面向资源的服务方式。