原文地址: http://martinfowler.com/articles/richardsonMaturityModel.html
Richardson 成熟模型
通往REST荣光的步骤
一个将rest做法的基本原则元素分成3个步骤的模型(Leonard Richardson发明的).这些介绍了资源(resources),http动作和超媒体控制(hypermedia controls).
最近我在读<Rest In Practice>(一本我几个同事最近在写的书)的草稿.他们的目的是解释如何用rest网络服务去解决许多企业会遇到的结合问题.这本书的中心思想是一个概念:网络是一个大量大规模的分布系统工作地很好的存在性证明.因此我们可以利用这个思想的一些主意去更简单地建立集合系统.
为了说明一个网络风格的系统的特殊属性,这些作者用了一个Leonard Richardson发明的Rest成熟模型和在一次QCon谈话上的解释.这个模型用简洁的方法去思考利用这三个工具,因此我想尝试一个自己的解释.(这些协议的例子只是用来说明,我不觉得它们值得写成代码并进行测试,因此在细节上会存在许多问题)
等级0
这个模型的起点是用http作为一个远程交互的传输系统,但是不用任何网络的机制.基本上你在这里做的就是把HTTP作为你远程交互系统的通道机制,一般基于远程程序调取(Remote Procedure Invocation).
让我们假设我希望和我的医生预约一次约定.我的预约软件首先需要知道我的医生在给定日期的开放位置,因此我发起一个请求到医院的约定系统获得这个信息.在等级0的场景中,医院将公开一个服务端点到一些URI上.我接着post到那个端点一个文档包含我的请求的细节.
POST /appointmentService HTTP/1.1
[various other headers]
<openSlotRequest date = "2010-01-04" doctor = "mjones" />
这个服务器将返回一个文档给我以下的信息:
HTTP/1.1 200 OK
[various headers]
<openSlotList>
<slot start = "!400" end = "1450" >
<doctor id = "mjones" />
</slot>
<slot start = "1600" end = " 1650">
<doctor id = "mjones" />
</slot>
</openSlotList>
我的例子用的是XML,但是内容可以是任何格式的:JSON,YAML,键值对或者任何自定义的类型.
我下一步将要定一个约定,我也是post一个文档到那个端点.
POST /appointmentService HTTP/1.1
[various other headers]
<appointmentRequest>
</slot doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith" />
</appointmentRequest>
如果一切顺利我将得到一个回复说我已经约定成功了.
HTTP/1.1 200 OK
[various headers]
<appointment>
<slot doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith" />
</appointment>
如果这中间有问题,比如有人在我之前约定了,那么我将获得一些错误信息在回复的正文里.
HTTP/1.1 200 OK
[various headers]
<appointmentRequestFailure>
<slot doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith" />
<reason>Slot not available</reason>
</appointmentRequestFailure>
到目前为止这是一个直截了当的RPC风格的系统.这很简单因为这仅仅是平常老套的XML来回传递.如果你用SOAP或者XML-RPC,基本是一样的机制,唯一的区别就是你将XML格式的信件用某种格式的信封封装了起来.
等级1
在RMM中通往Rest的荣光的第一步即是介绍资源.因此现在我们不再让我们的请求到单一的网络端点,而是开始和独立的资源交流.
因此对于我们的初始请求,我们可能有一个给定医生的资源.
POST /doctors/mjones HTTP/1.1
[various other headers]
<openSlotRequest date = "2010-01-04" />
这个请求包含了一些基本的信息,但是每一个开放时间现在成为了一个被强调的独立资源.
HTTP/1.1 200 OK
[various headers]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650" />
</openSlotList>
用特定的资源去预订意味着要post到特定的资源.
POST /slots/1234 HTTP/1.1
[various other headers]
<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>
如果一切顺利,那么将得到和之前类似的回复.
HTTP/1.1 200 OK
[various headers]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith"/>
</appointment>
不同之处在于如果有任何人需要对预约做任何事,类似于定一些测试,他们首先要get到约定这一个资源,可能通过一个类似于http://royalhope.nhs.uk/slots/1234/appointment的URI,并且post到那个资源.
对于像我这样的对象男士这类似于对象的识别的表达.不是调用方法传递参数,我们提供额外信息的参数去调用关于某个特定对象的方法.
等级2-HTTP动作
我已经在等级0和等级1的所有交互动作用了post动作,但是有些人使用get动作代替或者增加get动作.在那些等级这没有啥区别,它们都是被用作成通道机制运行你通过HTTP进行交互.等级2要驶离这一点了,运用HTTP动作尽可能和HTTP本身运用这些动作一样.
对于我们的开发时间列表,这意味着我们希望使用get动作.
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk
回复和之前用post动作时的回复一样
HTTP/1.1 200 OK
[various headers]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650" />
</openSlotList>
在等级2,在一个类似这个的请求中用get动作是很决断的.HTTP将get定义为了一个很安全的操作,它不会造成显著的改变状态或之类的事情.这允许我们调用get动作,无论是多少次,无论什么顺序,都会返回同样的结果.一个重要的结果就是这允许任何请求的路由可以使用缓存,这是使得网络表现地这么好的关键.HTTP有多种方法来支持缓存,这都能用于所有的通讯中.遵循HTTP的规则使得我们能够利用这种能力的优势.
去定一个约定我们需要能改变状态的HTTP动作,一个post动作或者put动作.我将和之前一样用post动作.
POST /slots/1234 HTTP/1.1
[various other headers]
<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>
运用post还是put的优劣势比我这里能讲到的要更复杂深入,也许我会再写一篇文章谈这一点.但是我想指出一些人没能正确理清POST/PUT和create/update之前的对应关系.他们之前的区别和那不同.
尽管我在等级1运用了同样的post,那里还有一个显著的区别关于远程服务是如何回应的,如果一切顺利,服务器将返回201码以表明一个新的资源在这个世界上创建了.
HTTP/1.1 201 Created
Location: slots/1234/appointment
[various headers]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>
这个201回复包含了一个URI位置特性,因此客户可以利用它去GET未来资源现在的状态.这个回复同时也含有一个那资源的表现,可以替用户节省一个额外的请求.
当时期出现错误时,这里还有个不同,比如另一个人在定这个会话.
HTTP/1.1 409 Conflict
[various headers]
<openSlotList>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>
这个回复重要的一部分是用HTTP状态码预示有些事出错了.比如是409是不错的选择去预示有人已经很矛盾地更新了这个资源.与其用200的返回码却包含一个错误信息,在等级2我们曾经明确地运用过这类的错误返回.这取决于协议设计者去决定用什么状态码,但是如果出现了错误不应该是2xx系列的回复.等级2介绍了HTTP动作和HTTP状态码.
这里有个不一致的前行.REST贡献者谈到了用所有的HTTP动作.他们也在证明他们说的REST试图学习网络的部分成功.但是全世界范围的网络在实践中不怎么使用PUT和POST结构.这有许多理由去多用PUT和PATCH,这里证明现有的网络不是其中之一.
被现在网络的存在性所支持的关键元素是在安全元素和非安全元素间的分离.结合在一起就是用状态码去帮助交流遇到的错误.
等级3-超媒体控制
最后一个等级将解释一些你经常听到在丑陋的首字母缩写词HATEOAS(超文本作为应用状态)下面的一些事情.它强调了如何从得到开放时间列表到知道如何预约的问题.
我们从和我们在等级2发送的同样的get请求开始.
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk
但是返回加上了一些新元素.
HTTP/1.1 200 OK
[various headers]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450">
<link rel = "/linkrels/slot/book" uri = "/slots/1234" />
</slot>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650">
<link rel = "/linkrels/slot/book" uri = "/slots/5678"/>
</slot>
</openSlotList>
每一个开放时间有一个链接元素包含了一个uri来告诉我们如何去预订一个约定.
超媒体的关键是它们告诉了我们下一步可以做什么和可以操作的资源的URI.不要求我们必须知道如何去post我们的预约请求,回复中的超媒体控制告诉了我们如何去做.
从等级2拷贝下来的post请求:
POST /slots/1234 HTTP/1.1
[various other headers]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>
回复包含了一些超媒体控制关于下一步可以做的不同事情.
HTTP/1.1 201 Created
Location: http://royalhope.nhs.uk/slots/1234/appointment
[various headers]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith" />
<link rel = "/linkrels/appointment/cancel" uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/addTest" uri = "/slots/1234/appointment/tests"/>
<ink rel = "self" uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/changeTime" uri = "/doctors/mjones/slots?date=20100104@status=open"/>
<link rel = "/linkrels/appointment/updateContactInfo" uri = "/patients/jsmith/contactInfo"/>
<link rel = "/linkrels/help" uri = "/help/appointment"/>
</appointment>
超媒体控制的一个很明显的优点在于允许服务器改变其URI计划而不破坏客户端.只要客户查找"addTest"链接的URI,接下来服务端可以改变所有的URI而不是始终用初始的入口.
一个更远的受益处在于它帮助了客户端的开发者探索协议.这些链接给了客户端的开发者一个下一步可能需要做什么的提示.它没有给所有的信息:"latest"和"cancel"控制都指向同一个URI-他们需要弄清楚其中一个是Get动作另一个是Delete动作.但是这至少给了他们一个起点去思考更多的信息和去协议文档看类似的URI.
类似的,它允许了服务端组去提倡新的能力通过放一个链接到回复中去.如果客户端的开发者关注这些未知的新链接,这些链接会触发他们未来新的探索.
这里没有绝对的标准如何表示超媒体控制.我这里做的只是用了<<Rest In Practice>>组目前的建议,他们服从了ATOM(RFC 4287).我用了一个包含为了特定URI的uri属性和描述关系的ref属性的link元素.一个广为人知的关系是赤裸的(比如self用了指向元素自己),任何为那个服务的明确都是一个完全合格的URI.ATOM状态定义了很有名的linkref是一种链接关系的注册.我写的这些受到ATOM可以做什么的限制,这在等级3restfuless中被看作成一个领导者.
这些等级的意义
我本应强调RMM尽管是一个很好的方式去思考REST的元素,却不是一种REST本身的等级定义.Roy Fielding在<<level 3 RMM is a pre-condition of REST>>中说的很清楚了.像在软件业的许多团队一样,REST有许多定义.但是因为Roy Fielding创造了这个术语,他的定义应该比其他的更重要一些.
我发现RMM有用之处在于其提供了一个一步步的方法去理解在restful思考背后的基本想法.从这点来说,我把它看作一个帮助我们学习概念的工具,而不是我们应该在任务机制中必须用的东西.我不确定目前我们有了足够的例子来确定restful做法是结合系统的正确做法.我真心觉得其是一种很有吸引力的做法以及我会在很多情况下推荐这种做法.
和Ian Robinson谈起这点,他强调当Leonard Richardson第一次提出这个模型时他发现非常有吸引力之处在于他和普遍的设计技术的联系.
- 等级1 利用分治去解决了处理复杂性的问题,将大规模的服务终点打碎成多个资源.
- 等级2 引入了标准的动作集合,因此我们可以用同样的方法处理类似的问题,移除不必要的改变.
- 等级3 引入了可发现性,提供了一种使得协议自己就是文档的方法.
结果是一种模型帮助我们思考我们想要提供的HTTP服务的种类和框起人们对和其交互的期望.