从"SOAP"到"REST"
最近有很多同仁问我,我们为什么要用REST?REST比SOAP好在哪?对于这个问题我想了不下十种答案。但转念一想,如何以一种最直接,类似于武侠小说中"一剑封喉"般的方式,"稳准狠"的解答他们的问题。就不至于就此展开一场"辩论赛"或是"科普贴"。好在问我这个问题的同仁大都不是程序猿界的"小鲜肉",大家对于时下软件研发的基本理念还是有共同认知的,基于此,我对这个问题的标准答案就是:"REST更OO"。
如果您还有兴趣深入了解REST和SOAP的区别,我觉得还是自行Google吧。
这篇文章想给大家介绍的重点更偏重于:我们如何从"SOAP"转向"REST",以及什么样的"RESTful API"才是真正的REST。
虽不赘述SOAP和REST的概念,但还是简单介绍一下何为SOAP,何为REST:
SOAP(Simple Object Access Protocol):简单对象访问协议
REST(Rerepresentational State Transfer):表示性状态转移
你在网上能搜到的异同点大致分为主要三个方面:
- SOAP是一种协议,而REST是一种规范
- SOAP支持多种传输协议,而REST目前仅支持HTTP/HTTPS
-
SOAP仅支持XML,而REST支持XML、Json甚至HTML
其实SOAP和REST严格来说不是两个对等的概念,我们姑且理解成两种服务设计思想及其具体的实现架构。
如果说SOAP和REST不够具象,那我来找两位代言人,自行感受一下我们所谓的"SOAP风格"和"REST风格"。
Web Service或是WCF大家想必不陌生,而Web Service和WCF就是利用SOAP协议进行的服务实现(当然他们也支持REST,但很鸡肋,不足以代言)。我们要对外提供服务离不开类似的技术。那我们是如何设计这样的服务接口的呢。说起来真是轻车熟路:我们先定义对象,而后我们设计接口。更有甚者,我们直接定义服务接口。所以一说到Web Service或是WCF我们就会想到接口,我们一看到接口,我们就非常的舒服。那我们为什么舒服,因为这很后台。进程间的接口调用方式总是会给程序员们以莫名的亲切感。
熟悉Web API风格的朋友可能就能想到REST。可Web API的服务应该怎么设计?如果我们还用上面说到设计Web Service/WCF的"SOAP风格"进行服务的设计,那REST比SOAP还有什么优势?仅仅是更轻量级的优势?
我们来举个例子:一只喵儿饿了,他要获取食物,食物分别有鱼和熊掌。所以我们要提供一个服务是食物的获取。基于这个简单的小例子,我们来看看从"SOAP"到"REST"的转型之路。
第一阶段:
我们提供一个为喵儿提供食物的REST服务。接到任务我们开始进行业务分析,最后确定2个方法:吃鱼和吃熊掌,对!就是这样一个简单粗暴的方法,我们想让喵儿调用这些方法,调用EatingFish可以获得鱼,调用EatingBearpaw可以获得熊掌。
我们的思路是:定义对象 --> 定义方法 改写成RESTful风格
很快RESTful API 被定义好了:Get http://service-root/EatingFish和Get http://service-root/EatingBearpaw
喵儿只要用GET请求这些URI就能得到食物。
然而这一版设计很快宣告失败。并不是我们对服务的功能定义出了问题,而是问题出在这个服务"很不RESTful",原因是:没有站在资源的角度考虑RESTful API的定义,而是延续了"接口"定义的"SOAP风格"。这样从"接口"入手,然后将"接口"以REST的URI形式暴露出来的API仍然是很"SOAP"的,这样定义出的RESTful API,一般是Level 0或Level 1的。
那我们就来介绍一下REST的4个境界,我也称之为REST的"成熟度模型"。
Level 0:没有明确的资源概念,只有一个URL,只是用单个HTTP方法
Level 1:有明确的资源概念,存在很多URL,只是用单个HTTP方法
Level 2:有明确的资源概念,存在很多URL,使用HTTP作为资源的统一接口
Level 3:在满足2级标准的基础上,使用超媒体作为应用状态的引擎
我们再来回过头看看我们的表达Get http://service-root/EatingFish,无疑是Level 0。
即便是我们生硬的在"EatingFish"前将"Food"表达出来也不过是Level1的表达:Get http://service-root/Food/EatingFish
分别参照一下Level 0和Level 1的定义,我们发现Level0的表达没有"资源"的概念,仍然是提供了一个"行为"的"接口",这种做法像极了"SOAP"。我们再来看看Level 1的表达,虽然已经明确出了"Food"这个资源,Food资源下也可以定义多个针对Food的方法,但仍然定义的是诸如"EatingFish"和"EatingBearPaw"这样的一个个行为来作为资源的方法。
第二阶段:
我们重新思考,要站在"资源"的角度考虑这个服务,而资源的CRUD又可以通过HTTP的POST、GET、PUT和DELETE请求表达。也就是说我们只要把"资源"表述清楚,利用REST的理念CRUD我们就不必考虑了。基于这样的考虑,我们赶快调整思路:定义资源 --表述资源
资源定义:
我们定义了三个资源"Food"作为食物的集合,而"Food"下,有"Fish"和"BearPaw",对于资源的表述我们采取了Collection+Json的超媒体格式。
资源表述:
{
"collection": {
"version": "1.0",
"href": "http://service-root/Food",
"links": [ ],
"items": [
{
"href": "/Fish",
"data": [
{
"name": "Name",
"value": "鱼"
},
{
"name": "Code",
"value": "Fish"
}
],
"links": [ ]
},
{
"href": "/ BearPaw ",
"data": [
{
"name": "Name",
"value": "熊掌"
},
{
"name": "Code",
"value": " BearPaw "
}
],
"links": [ ]
}
],
"queries":[ ],
"template":{
"data": [
{
"name": "Name",
"value": ""
},
{
"name": "Code",
"value": ""
}
]
},
"error": {
"code": "",
"Message": ""
}
}
}
根据资源的定义,通过Collection+Json这种超媒体类型进行资源的表述。使用HTTP协议语义规定的请求类型作为资源的统一接口,不需要再像Level 0 和Level 1中那样单独定义或描述接口。
这时喵儿如果想要吃鱼,只需要GET http://service-root/Food/Fish他就能获得鱼。而如果我们想要从食物中把熊掌删除掉,也只需要 DELETE http://service-root/Food/BearPaw。如果我们想帮喵增加一种食物-肉,只需要POST http://service-root/Food/Meat,同时利用资源表述中的模板template,将肉的信息传回去,肉这种食物就被添加进了食物中。
这时我们发现我们再去思考服务的设计已经不是站在"要提供什么样的方法"这种基于过程、基于行为的角度去思考。而是基于"资源"的面向对象的设计方法。
做到这个程度,感觉已经非常的RESTful了,但回到REST的本意看看,就体会出了问题的所在。回观REST的定义:REST(Rerepresentational State Transfer)表示性状态转移,多读几遍我们就渐渐的感觉到了一个词:"转移"。
而再对照"成熟度模型"我们发现这时候的API已经是Level 2的。Level 2到Level 3,如何表示"转移"成了下一个阶段进阶的关键。
为了更好的展现出第三个阶段的特点,我们现在扩充一下这个小例子,在喵儿获取过食物之后,他还想来点甜点,甜点有蛋糕和冰激凌。(真是只贪得无厌的喵儿)
第三阶段:
继续调整思路:定义资源 --定义资源的链接关系 --> 资源表述
第一步和第三步与第二阶段没有什么差别,而关键在于第二步,REST的本意希望能够通过资源的表述,描述出每个资源与其他资源的链接关系。
回到我们的例子,我们定义资源,这时候会有两棵树,"食物"和"甜点":
而接下来,我们希望喵儿在获得到食物之后,能够知道接下来有甜点吃。这时我们引入了资源的状态图:
资源的状态图表述的是从当下的资源能够链接到哪个资源。我们可以看到当获取到Fish之后,喵儿就能看到有Desserts,当他访问Desserts时,就能看到为他准备好的Cake和Ice Cream了。
那这种链接如何表述呢?
我们观察到不管是REST的哪一种超媒体格式,都为我们准备了Link字段:
{
"collection": {
"version": "1.0",
"href": "http://service-root/Food ",
"links": [ ],
"items": [
{
"href":"/Fish",
"data": [
{
"name":"Name",
"value":"鱼"
},
{
"name":"Code",
"value":"Fish"
}
],
"links": [
{
"rel":"Desserts",
"href":" http://service-root/Desserts",
"prompt": "甜点"
}
]
},
{
"href":"/ BearPaw ",
"data": [
{
"name":"Name",
"value":"熊掌"
},
{
"name":"Code",
"value":" BearPaw "
}
],
"links":[
{
"rel":"Desserts",
"href":" http://service-root/Desserts",
"prompt": "甜点"
}
]
}
],
"queries": [ ],
"error": {
"code": "",
"Message": ""
}
}
}
在Level 2 的基础上,利用Collection+Json中对Links的表达,表述了资源间转移的关系。如有必要同时利用Queries表达了对资源集合的过滤。至此我们完成了从"SOAP"到"REST"的转变。
一些注意:
1、 定义资源不是在定义逻辑模型,更不是E-R;
2、 一个服务中描述的资源不一定是同一根的,"转移"也可以发生在两棵树之间。
3、 一旦出现了不用Level0 和Level 1就表达不了的情况,基本上就是资源的定义出了问题
4、 REST并不是"银弹",它解决不了你资源(对象)设计本身的问题。