0%

Richard成熟度模型

Leonard Richardson的模型将REST的基本元素分解到三个步骤当中。这三个步骤包括:资源(resources),http谓词(get, post, put, delete, head, option),超链接控制。

注:文章译自Martin Fowler博客,原文链接.
最近我(Martin Fowler)阅读了一本《Rest In Praces》,我的同事们写的一本书。他们的目的是解释如何使用Rest Web Service来解决企业级应用于道的集成难题。这本书的核心来自于一个观察,即当前的互联网(Web)就是一个大规模的分布式系统,并且它运行良好,所以我们可以借鉴互联网(Web)的组织方式来构建我们的集成系统。
Figure 1: REST的层次

为了更好的解释类互联网(web-style)系统的详细属性,(Rest In Practise)作者们使用了restfult成熟度模型(注:这一模型由Leonard Richardson发现,并在QCon上提出)。该模型采用了很好的方式来思考如何使用Web相关的技术。所以我(Martin Fowler)会以自己的方式来解释一下Rest成熟度模型(下面的例子只是用作说明,不值得拿去编码测试)。

# Level 0

起点是将HTTP作为远程交互的传输协议,而不使用Web相关的机制。实际上,这种方式只是讲HTTP座位一个通讯管道,而其中包含的是自定义的远程交互机制,通常是基于Remote Procedure Invocation。
Figure 2: 使用Level 0的交互样例

假设我要预约一下我的医生。我的预约软件首先需要知道,我的医生在特定日期哪个时段是可约的,所以它像医院的预约系统发送请求,以查看该信息。在Level 0的场景下,医院会在某个url上开放一个服务接入点(service endpoint)。我会向该接入点发送一个包含了我详细请求的文档。

1
2
3
4
POST /appointmentService HTTP/1.1
[various other headers]

<openSlotRequest date = "2015-06-16" doctor="mjones">

医院的服务器则会给我返回如下信息:
1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
[various headers]

<openSlotList>
<slot start = "14:00", end = "14:50">
<doctor id = "mjones" />
</slot>
<slot start = "16:00", end = "16:50">
<doctor id = "mjones"/>
</slot>
</openSlotList>

我使用了XML座位例子,但这个内容可以是任何形式:JSON,YAML,key-Value Pairs,或是其他的自定义格式。
下面,我需要预约一个时段,同样的我可以想服务接入点发送一个请求。
1
2
3
4
5
6
7
POST /appointmentService HTTP/1.1
[various other headers]

<appointmentRequest>
<slot doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
</appointmentRequest>

如果一切顺利,我将会得到反馈说我的预约成功了。
1
2
3
4
5
6
7
HTTP/1.1 200 OK
[various headers]

<appointment>
<slot doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
</appointment>

如果遇到了问题,例如另一个人在我之前预约了,我将得到一个错误反馈。
1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
[various headers]

<appointmentRequestFailure>
<slot doctor = "mjones" start="14:00" end = "14:50"/>
<patient id = "jsmith" />
<reason>Slot not available</reason>
</appointmentRequestFailure>

到现在为止,仍是一个直观的RPC类系统。它只是在把XML发送与返回。如果你使用SOAP或者XML-RPC,基本与此类似,唯一的区别就是把你的XML信息包装在了一个“信封”中。

# Level 1 - Resources

走向REST的在RMM中巅峰的第一步是引入资源(resources)。那么,现在与其将所有的请求都发往一个单独的服务接入点,我们现在可以想独立的资源发送请求了。
Figure 3: Level1 增加了资源(resource)
那么对于我们的第一个请求,可能有一个资源代表指定的医生。

1
2
3
4
POST /doctors/mjones HTTP/1.1
[various other headers]

<openSlotRequest date = "2015-06-16">

响应携带类似的基本信息,但是每个slot都是一个可以被单独请求。
1
2
3
4
5
6
7
HTTP/1.1 200 OK
[various headers]

<openSlotList>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50">
<slot id = "5678" doctor = "mjones" start = "16:00" end = "16:50">
<openSlotList/>

有了对应的资源之后,预约一个时段意味着向对应的slot发送请求。
1
2
3
4
5
6
POST /slots/1234 HTTP/1.1
[various other headers]

<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>

如果一起顺利,会得到一个类似下述的响应。
1
2
3
4
5
6
7
HTTP/1.1 200 OK
[various headers]

<appointment>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
</appointment>

与Level0的差别是,如果有谁想对appointment做些什么,像预定一些化验,他们需要先拿到相应的appointment资源,该资源可能有个URI,像http://roylhope.nhs.uk/slots/1234/appointment,并对此URL发送请求。

对于像我这样的面相对象的人来说,资源特别像是对象的唯一标示符。相比于调用一个方法并传递(所有信息)参数,我们调用一个指定对象上的方法并提供其他相关的参数。

# Level 2 - HTTP Verbs
在上述(Level0 和 Level1)的所有请求中,我都适用了HTTP的POST谓词,但是一些人会使用GET取而代之。在这些层面上(Level0和Level)并不会产生什么不同,他们都用来作为你在HTTP上传输交互信息的管道。Level2 比此更进了一步,使用HTTP的谓词来表征将会对响应的资源(resource)产生怎样的操作。
Figure 4 Level 2 增加HTTP谓词语义
对于我们想获得医生的空闲时段,意味着我们需要使用GET。

1
2
GET /doctors/mjones/slots?date=20150616&status=open HTTP/1.1
Host: royalhope.nhs.uk

得到的响应和使用POST一致。
1
2
3
4
5
6
7
HTTP/1.1 200 OK
[various headers]

<openSlotList>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50">
<slot id = "5678" doctor = "mjones" start = "16:00" end = "16:50">
<openSlotList/>

在Level2中,对于GET的使用是个关键。HTTP定义GET是一个安全操作,也就是说它不会对任何事物的状态引起任何特别的影响。这允许我们安全的任意次数的以任意顺序的执行GET,并得到相同的结果。一个重要的结果的是,它允许请求链路上的任何一个参与者使用缓存,而缓存恰恰是web性能的重要优化方式。HTTP包含多种方式支持缓存,这些可以被通讯中的所有参与者所使用。通过遵从HTTP的原则,我们能够利用它提供的能力。

为了预约一个时段,我们需要一个HTTP位次来改变(响应资源)状态,一个POST或是一个PUT。我会像之前一样使用POST。

1
2
3
4
5
6
POST /slots/1234 HTTP/1.1
[various other headers]

<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>

POST和PUT之间的取舍超出了本文的范围,可能需要一篇新的文章来说明这个问题。但是我想指出一些人错误的映射了POST/PUT和CREATE/UPDATE之间的对应关系。他们之间的选择则是与这种映射关系更为不同。

尽管如果我使用了与Lelvel 1一样的POST请求,然而服务端的反馈则会有明显的不同。如果一切顺利,服务会返回一个带有201状态码的响应,201代表这有一个新的资源产生了。

1
2
3
4
5
6
7
8
HTTP/1.1 201 Created
Location: slots/1234/appointment
[various headers]

<appointment>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
</appointment>

201状态码表示了一个location参数,并携带了一个URI,该URI可以被客户端使用GET来查看资源状态。响应中也包含了新增资源的描述,可以引导客户端发送一个附加的请求。

当发生错误时还会产生另一种不同,例如某人已经预定了该时段。

1
2
3
4
5
6
HTTP/1.1 409 Conflict
[various headers]

<openSlotList>
<slot id = "5678" doctor = "mjones" start = "16:00" end = "16:50" />
<openSLotList>

这次响应中重要的部分是使用HTTP的状态码来表征发生了错误。在这种情况下,用409来表示其他人已经更新了该资源看起来是一个比较合适的选择。相比于返回一个状态码是200,但是内容中包含了错误信息,在Level 2中,我们显示的使用这种方式来表达错误的响应。具体使用什么状态码来表征什么反馈是协议设计者的事情,但是至少应该要采用一个非2XX的状态码来表征错误。Level 2 引入了HTTP的谓词和状态码。

这里有一个不一致的地方。REST建议使用所有的HTTP谓词。他们也在证实他们的方式是借鉴自Web系统的成功实践。但是万维网不怎么使用PUT和DELETE。有很多的理由来使用PUT和DELETE,但Web的经验并不是其中之一。

当前的Web最为重要的元素是强烈的区分开安全操作(GET)和非安全操作(POST),并且使用状态码来标示是否产生了错误。

# Level 3-Hypermedia Controls
最后一个层次引入了大家耳熟能详的拥有丑陋缩写的HATEOAS(Hypertext As The Engine Of Application State)。它将问题从如何获取“空闲时段列表”转换成了要做什么才能预定到一个时段(抽象啊)。
Figure 5 : Level 3 增加了超链接控制
我们从最开始的GET请求入手,和在Level 2中发送的请求一样。

1
2
GET /doctors/mjones/slots?date=20150616&status=open HTTP/1.1
Host: royalhope.nhs.uk

但是响应中有一个新的元素:
1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
[various other headers]

<openSlotList>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50">
<link rel = "/linkrels/slot/book" uri = "/slots/1234" />
</slot>
<slot id = "5678" doctor = "mjones" start = "16:00" end = "16:50">
<link rel = "/linkrels/slot/book" uri = "/slots/1234" />
</slot>
<openSlotList/>

每个slot中包含了一个link元素(包含个URI),用以表明可以怎样去预定一次预约。
超链接控制的关键点在于告诉我们下一步要做什么,以及下一步操作所需资源的URI。相对于要我们自己去了解要向哪里发送预约请求,超链接控制则是告诉我们怎么去做。

这个POST请求根Level 2中的一样。

1
2
3
4
5
6
POST /slots/1234 HTTP/1.1
[various other headers]

<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>

而响应中则包含了一系列的超链接控制来表征下一步可以做的多个行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/1.1 201 Created
Location: slots/1234/appointment
[various headers]

<appointment>
<slot id = "1234" doctor = "mjones" start = "14:00" end = "14:50" />
<patient id = "jsmith" />
<link rel = "/linkrels/appointment/cancel" uri = "/slots/1234/appointment" />
<link rel = "/linkrels/appointment/addTest" uri = "/slots/1234/appointment/tests" />
<link rel = "self" uri = "/slots/1234/appointment" />
<link rel = "/linkrels/appointment/changeTime" uri = "/doctors/mjones/slots?date=20150616&status=open" />
<link rel = "/linkrels/appointment/updateContactInfo" uri = "/patients/jsmith/contactInfo" />
<link rel = "/linkrels/help" uri = "/help/appointment" />
</appointment>

超链接控制的一个显著好处是它允许服务端改变他自己的URI配置的同时不会对客户端引发影响。一旦用户寻找“addTest”的URI时,服务端可以隐藏掉所有其他的初始接入点。
一个附加的好处是允许客户端开发者了解整个协议。这些链接会给客户端开发者一个提示,“接下来你可能需要做什么”。他并不提供所有的信息,例如“latest”和“cancel”都指向相同的URI,他们需要明确一个是GET而另一个是DELETE。但是呐,这样至少给客户端开发人员一个出发点,让他们知道要去文档中找什么。
同样的,超链接控制允许服务端开发人员通过讲新的link加入响应的方式增加新功能。

到目前为止们还没有完整的标准来指导大家应该如何表征超链接控制。我当前所做的则是按照《REST in Practise》中推荐的那样(而他是遵从ATOM,RFC 4287的)。我使用元素,其中包含uri属性(其标示了目标URI)以及rel属性(其标示出了关系的类型)。一个通用的关系(例如self用来指向资源本身)是十分明显的,任何其他特定于该服务端的link都是一个合格的URI。ATOM表明熟知的linkrels是Registry Of Link Relations。我写的这些受到ATOM限制的,ATOM本身是在第三层restfulness的领导者。

# 分层的意义
我应该强调的是,RMM,尽管它是一个思考REST相关元素的好方式,但并不是REST层次的定义本身。[Roy Fielding说明了3层RMM是REST的一个前置条件](http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven]。像软件中的许多定义一样,REST有很多的定义,但是由于Roy Fielding发现了这一概念,他的定义应该比其他的更具权威性。
我发现,RMM提供了一个逐步递进的方式来帮助我们理解restful 这种想法背后的基本概念。因此,我把RMM视为一种帮助我们理解概念的工具而不是某种评价验证体系。我觉得使用Restful来集成系统的实际案例还不足以证实它是最好的。但是我确实认为它是一个吸引人的方案,并且在大多数情况下我会推荐这种方案。
与lan Robinson讨论,它强调RMM真正吸引人的地方在于它引入了普通的程序设计理念。

* Level 1,通过讲一个大的服务接入点打碎成为许多的资源,来降低问题的复杂性。此处运用了分治的思想。
* Level 2,引入了一些列的标准谓词,所以我们可以用同样的方式来处理类似的问题。此处消除了不必要的变量。
* Level 3,引入了发现能力,提供了一种方式让协议可以自解释。此处引入了自解释能力。

本文译自Martin Fowler博客,原文链接