抢号专区

【阅读笔记】REST设计风格

Less than a minute to read

医生预约系统作为一名病人,我想要从系统中得知指定日期内我熟悉的医生是否具有空闲时间,以便于我向该医生预约就诊。

第0级

医院开放了一个/appointmentService的Web API,传入日期、医生姓名等参数,可以得到该时间段内该名医生的空闲时间,该API的一次HTTP调用如下所示:

1 POST /appointmentService?action=query HTTP/1.1

2

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

然后服务器会传回一个包含了所需信息的回应:

1 HTTP/1.1 200 OK

2

3 [

4 {start:"14:00", end: "14:50", doctor: "mjones"},

5 {start:"16:00", end: "16:50", doctor: "mjones"}

6 ]

得到了医生空闲的结果后,笔者觉得14:00比较合适,于是进行预约确认,并提交了个人基本信息:

1 POST /appointmentService?action=comfirm HTTP/1.1

2

3 {

4 appointment: {date: "2020-03-04", start:"14:00", doctor: "mjones"},

5 patient: {name: icyfenix, age: 30, ……}

6 }

如果预约成功,那我能够收到一个预约成功的响应:

1 HTTP/1.1 200 OK

2

3 {

4 code: 0,

5 message: "Successful confirmation of appointment"

6 }

如果出现问题,譬如有人在我前面抢先预约了,那么我会在响应中收到某种错误消息:

1 HTTP/1.1 200 OK

2

3 {

4 code: 1

5 message: "doctor not available"

6 }

至此,整个预约服务宣告完成,直接明了,我们采用的是非常直观的基于RPC风格的服务设计,似乎很容易就解决了所有问题,但真的是这样吗?

第1级第0级是RPC的风格,如果需求永远不会变化,那它完全可以良好地工作下去。但是,如果你不想为预约医生之外的其他操作、为获取空闲时间之外的其他信息去编写额外的方法,或者改动现有方法的接口,那还是应该考虑一下如何使用REST来抽象资源。通往REST的第一步是引入资源的概念,在API中的基本体现是围绕资源而不是过程来设计服务,说得直白一点,可以理解为服务的Endpoint应该是一个名词而不是动词。此外,每次请求中都应包含资源的ID,所有操作均通过资源ID来进行,譬如,获取医生指定时间的空闲档期:

1 POST /doctors/mjones HTTP/1.1

2

3 {date: "2020-03-04"}

然后服务器传回一组包含了ID信息的档期清单,注意,ID是资源的唯一编号,有ID即代表“医生的档期”被视为一种资源:

1 HTTP/1.1 200 OK

2

3 [

4 {id: 1234, start:"14:00", end: "14:50", doctor: "mjones"},

5 {id: 5678, start:"16:00", end: "16:50", doctor: "mjones"}

6 ]

笔者还是觉得14:00的时间比较合适,于是又进行预约确认,并提交了个人基本信息:

1 POST /schedules/1234 HTTP/1.1

2

3 {name: icyfenix, age: 30, ……}

后面预约成功或者失败的响应消息在这个级别里面与之前一致,就不重复了。

比起第0级,第1级的特征是引入了资源,通过资源ID作为主要线索与服务交互,但第1级至少还有三个问题没有解决:

一是只处理了查询和预约,如果临时想换个时间,要调整预约,或者病忽然好了,想删除预约,这都需要提供新的服务接口;

二是处理结果响应时,只能依靠结果中的code、message这些字段做分支判断,每一套服务都要设计可能发生错误的code,这很难考虑全面,而且也不利于对某些通用的错误做统一处理;

三是没有考虑认证授权等安全方面的内容,譬如要求只有登录用户才允许查询医生档期时间,某些医生可能只对VIP开放,需要特定级别的病人才能预约,等等。

第2级第1级遗留的三个问题都可以通过引入统一接口来解决。

HTTP协议的七个标准方法是经过精心设计的,只要架构师的抽象能力够用,它们几乎能涵盖资源可能遇到的所有操作场景。

REST的具体做法是:把不同业务需求抽象为对资源的增加、修改、删除等操作来解决第一个问题;

使用HTTP协议的Status Code,它可以涵盖大多数资源操作可能出现的异常,也可以自定义扩展,以此解决第二个问题;

依靠HTTP Header中携带的额外认证、授权信息来解决第三个问题,这个在实战中并没有体现,后文会在5.3节中介绍相关内容。

按这个思路,获取医生档期,应采用具有查询语义的GET操作进行:

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"}

]

笔者仍然觉得14:00的时间比较合适,于是进行预约确认,并提交了个人基本信息,用以创建预约,这是符合POST的语义的:

POST /schedules/1234 HTTP/1.1

{name: icyfenix, age: 30, ......}

如果预约成功,那笔者能够收到一个预约成功的响应:

HTTP/1.1 201 Created

Successful confirmation of appointment

如果出现问题,譬如有人抢先预约了,那么笔者会在响应中收到某种错误消息:

HTTP/1.1 409 Conflict

doctor not available

第3级第2级是目前绝大多数系统所到达的REST级别,但仍不是完美的。

至少还存在一个问题:你是如何知道预约mjones医生的档期是需要访问“/schedules/1234”这个服务Endpoint的?

也许你第一时间甚至无法理解为何我会有这样的疑问,这当然是程序代码写的呀!

但REST并不认同这种已烙在程序员脑海中许久的想法。

RMM中的超文本控制、Fielding论文中的HATEOAS和现在提的比较多的“超文本驱动”,所希望的是除了第一个请求是由你在浏览器地址栏输入驱动之外,其他的请求都应该能够自己描述清楚后续可能发生的状态转移,由超文本自身来驱动。

所以,当你输入了查询的指令之后:

GET /doctors/mjones/schedule?date=2020-03-04&status=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"}

]

}

如果做到了第3级REST,那服务端的API和客户端也是完全解耦的,此时如果你要调整服务数量,或者对同一个服务做API升级时将会变得非常简单。


Copyright © 2088 时效性网游活动中心 All Rights Reserved.
友情链接