REST设计风格:你写的 RESTful API 到第几层了?

理解REST

在理解其真正概念前,咱们先来明确: REST它的核心思想是面向资源的抽象(相对于RPC就是面向过程抽象),它是一种设计风格的指导,而非具备较强约束的协议。前端

REST源于Roy Thomas Fielding在2000年发表的博士论文“Architectural Stylesand the Design of Network-based Software Architectures”[1]提出的一种编程思想,并为这种程序设计风格取了一个不少人难以理解,可是今天已经广为人知的名字——REST(Representational State Transfer,表征状态转移)。程序员

若是拆分红独立单词RepresentationalStateTransfer,咱们知道它们分别是表征状态转移的意思。可是合在一块儿,好像又不明白它想要表达的意思了。咱们不妨先去理解什么是HTTP(毕竟REST是创建在HTTP之上的),你会发现REST其实是“HTT”(Hypertext Transfer)的进一步抽象,二者的关系就如同接口与实现类的关系通常。REST是对资源的抽象,何为资源?web

资源(Resource):譬如你如今正在阅读一篇名为《REST设计》的文章,这篇文章的内容自己(你能够将其理解为蕴含的信息、数据)称之为“资源”。不管你是经过阅读购买的图书、浏览器上的网页仍是打印出来的文稿,不管是在电脑屏幕上阅读仍是在手机上阅读,尽管呈现的样子各不相同,但其中的信息是不变的,你所阅读的还是同一份“资源”。数据库

而后咱们以此文章为资源,来看看表征状态转移在阅读文章过程的中含义:编程

表征(Representation):当你经过浏览器阅读此文章时,浏览器会向服务端发出“我须要这个资源的HTML格式”的请求,服务端向浏览器返回的这个HTML就被称为“表征”,你也能够经过其余方式拿到本文的PDF、Markdown、RSS等其余形式的版本,它们一样是一个资源的多种表征。可见“表征”是指信息与用户交互时的表示形式,这与咱们软件分层架构中常说的“表示层”(PresentationLayer)的语义实际上是一致的。json

状态(State):当你读完了这篇文章,想看后面是什么内容时,你向服务端发出“给我下一篇文章”的请求。可是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”才能正确回应,这类在特定语境中才能产生的上下文信息被称为“状态”。咱们所说的有状态(Stateful)抑或是无状态(Stateless),都是只相对于服务端来讲的,服务端要完成“取下一篇”的请求,要么本身记住用户的状态,如这个用户如今阅读的是哪一篇文章,这称为有状态;要么由客户端来记住状态,在请求的时候明确告诉服务端,如我正在阅读某某文章,如今要读它的下一篇,这称为无状态。后端

转移(Transfer):不管状态是由服务端仍是由客户端来提供,“取下一篇文章”这个行为逻辑只能由服务端来提供,由于只有服务端拥有该资源及其表征形式。服务端经过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。浏览器

RESTful系统的六大设计原则

1.客户端与服务端分离(Client-Server)

将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来,有助于提升用户界面的跨平台的可移植性。相较以往的彻底基于服务端控制和渲染(如JSP这类)的模式已甚少,一方面代码仓库的便捷性和易管理性成为了敏捷开发的障碍,另外一方面得益于前端技术(从ES规范,到语言实现,再到前端框架等)在近年来的高速发展,造就了现现在的**先后端分离*模式:后端控制数据,前端控制渲染。缓存

2.无状态(Stateless)

REST但愿服务端不用负责维护状态,每一次从客户端发送的请求中,应包括全部必要的上下文信息,会话信息也由客户端负责保存维护,服务端只依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。安全

但现实是骨感的,大型系统的上下文状态数量彻底可能膨胀到客户端没法承受的程度,在服务端的内存、会话、数据库或者缓存等地方持有必定的状态成为一种事实上存在,并将长期存在、被普遍使用的主流方案。

3.可缓存(Cacheability)

无状态服务虽然提高了系统的可见性、可靠性和可伸缩性,但下降了系统的网络性。“下降网络性”的通俗解释是某个功能使用有状态的设计时只须要一次(或少许)请求就能完成,使用无状态的设计时则可能会须要屡次请求,或者在请求中带有额外冗余的信息。为了缓解这个矛盾,REST但愿软件系统可以如同万维网同样,容许客户端和中间的通讯传递者(譬如代理)将部分服务端的应答缓存起来。固然,为了缓存可以正确地运做,服务端的应答中必须直接或者间接地代表自己是否能够进行缓存、能够缓存多长时间,以免客户端在未来进行请求的时候获得过期的数据。运做良好的缓存机制能够减小客户端、服务端之间的交互,甚至有些场景中能够彻底避免交互,这就进一步提升了性能。

4.分层系统(Layered System)

这里所指的分层并非表示层、服务层、持久层这种意义上的分层,而是指客户端通常不须要知道是否直接链接到了最终的服务器,抑或链接到路径上的中间服务器。中间服务器能够经过负载均衡和共享缓存的机制提升系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。该原则的典型应用是内容分发网络(ContentDistribution Network,CDN)。若是你是经过网站浏览到这篇文章的话,你所发出的请求通常(假设你在中国境内的话)并非直接访问位于GitHub Pages的源服务器,而是访问了位于国内的CDN服务器,但做为用户,你彻底不须要感知到这一点。

5.统一接口(Uniform Interface)

这是REST的另外一条核心原则,REST但愿开发者面向资源编程,但愿软件系统设计的重点放在抽象系统该有哪些资源,而不是抽象系统该有哪些行为(服务)上。这条原则你能够类比计算机中对文件管理的操做来理解,管理文件可能会涉及建立、修改、删除、移动等操做,这些操做数量是可数的,并且对全部文件都是固定、统一的。若是面向资源来设计系统,一样会具备相似的操做特征,因为REST并无设计新的协议,因此这些操做都借用了HTTP协议中固有的操做命令来完成。

统一接口也是REST最容易陷入争论的地方,基于网络的软件系统,究竟是面向资源合适,仍是面向服务更合适,这个问题恐怕在很长时间里都不会有定论,也许永远都没有。可是,已经有一个基本清晰的结论是:面向资源编程的抽象程度一般更高。抽象程度高带来的坏处是距离人类的思惟方式每每会更远,而好处是通用程度每每会更好。用这样的语言去诠释REST,仍是有些抽象,下面以一个例子来讲明:譬如,对于几乎每一个系统都有的登陆和注销功能,若是你理解成登陆对应于login()服务,注销对应于logout()服务这样两个独立服务,这是“符合人类思惟”的;若是你理解成登陆是PUT Session,注销是DELETE Session,这样你只须要设计一种“Session资源”便可知足需求,甚至之后对Session的其余需求,如查询登陆用户的信息,就是GET Session而已,其余操做如修改用户信息等也均可以被这同一套设计囊括在内,这即是“抽象程度更高”带来的好处。

若是想要在架构设计中合理恰当地利用统一接口,Fielding建议系统应能作到每次请求中都包含资源的ID,全部操做均经过资源ID来进行;建议每一个资源都应该是自描述的消息;建议经过超文原本驱动应用状态的转移

6.按需代码(Code-On-Demand)

按需代码被Fielding列为一条可选原则。它是指任何按照客户端(譬如浏览器)的请求,将可执行的软件程序从服务端发送到客户端的技术。按需代码赋予了客户端无须事先知道全部来自服务端的信息应该如何处理、如何运行的宽容度。举个具体例子,之前的Java Applet技术,今天的WebAssembly等都属于典型的按需代码,蕴含着具体执行逻辑的代码是存放在服务端,只有当客户端请求了某个JavaApplet以后,代码才会被传输并在客户端机器中运行,结束后一般也会随即在客户端中被销毁。将按需代码列为可选原则的缘由并不是是它特别难以达到,更可能是出于必要性和性价比的实际考虑。

RMM (Richardson MaturityModel)

RESTful Web APIs和RESTful Web Services的做者Leonard Richardson曾提出一个衡量“服务有多么REST”的Richardson成熟度模型(Richardson MaturityModel,RMM),以便让那些本来不使用REST的系统,可以逐步地导入REST。Richardson将服务接口“REST的程度”从低到高,分为0至3级。

  • 第0级(The Swamp of Plain Old XML):彻底不REST。
  • 第1级(Resources):开始引入资源的概念。
  • 第2级(HTTP Verbs):引入统一接口,映射到HTTP协议的方法上。
  • 第3级(Hypermedia Controls):超媒体控制,在本文里面的说法是“超文本驱动”,在Fielding论文里的说法是“Hypertext As The Engine Of ApplicationState,HATEOAS”,其实都是指同一件事情。

下面借用Martin Fowler撰写的关于RMM的文章中的实际例子(原文是XML写的,这里简化为JSON表示),来具体展现一下四种不一样程度的REST反映到实际接口中会是怎样的。假设你是那名程序员,你会怎么设计:

医生预定系统

做为一名病人,我想要从系统中得知指定日期内我熟悉的医生是否具备空闲时间,以便于我向该医生预定就诊。 请设计两个RESTful接口:一个查询空闲时间接口,一个预定就诊接口。

第0级

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

POST /appointmentService?query HTTP/1.1

{"data": "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"} ] 复制代码

获得了医生空闲的结果后,笔者以为14:00比较合适,因而进行预定确认,并提交了我的基本信息:

POST /appointmentService?action=confirm HTTP/1.1

{ "appointment": {"date": "2020-03-04", "start":"14:00", "end":"14:50", "doctor": "mjones"}, "patient": {"name": "zio", "age": 30, ...} } 复制代码

若是预定成功,那我可以收到一个预定成功的响应:

HTTP/1.1 200 OK

{ "code": 0, "message": "Successful confirmation of appiontment" } 复制代码

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

HTTP/1.1 200 OK

{ "code": 1, "message": "doctor not available" } 复制代码

至此,整个预定服务宣告完成,直接明了,咱们采用的是很是直观的基于RPC风格的服务设计

第1级

第0级是RPC的风格,若是需求永远不会变化,那它彻底能够良好地工做下去。可是,若是你不想为预定医生以外的其余操做、为获取空闲时间以外的其余信息去编写额外的方法,或者改动现有方法的接口,那仍是应该考虑一下如何使用REST来抽象资源。

通往REST的第一步是引入资源的概念,在API中的基本体现是围绕资源而不是过程来设计服务,说得直白一点,能够理解为服务的Endpoint应该是一个名词而不是动词。此外,每次请求中都应包含资源的ID,全部操做均经过资源ID来进行,譬如,获取医生指定时间的空闲档期:

GET /doctors/mjones?date="2020-03-04" HTTP/1.1
复制代码

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

HTTP/1.1 200 OK

[ {"id": 1, "start":"14:00", "end":"14:50", "doctor": "mjones"}, {"id": 2, "start":"16:00", "end":"16:50", "doctor": "mjones"} ] 复制代码

笔者仍是以为14:00的时间比较合适,因而又进行预定确认,并提交了我的基本信息:

POST /schedules/1 HTTP/1.1

{"name": "zio", "age":30, ...} 复制代码

后面预定成功或者失败的响应消息在这个级别里面与以前一致,就不重复了。比起第0级,第1级的特征是引入了资源,经过资源ID做为主要线索与服务交互,但第1级至少还有三个问题没有解决:一是只处理了查询和预定,若是临时想换个时间,要调整预定,或者病突然好了,想删除预定,这都须要提供新的服务接口;二是处理结果响应时,只能依靠结果中的code、message这些字段作分支判断,每一套服务都要设计可能发生错误的code,这很难考虑全面,并且也不利于对某些通用的错误作统一处理;三是没有考虑认证受权等安全方面的内容,譬如要求只有登陆用户才容许查询医生档期时间,某些医生可能只对VIP开放,须要特定级别的病人才能预定,等等。

第二级

第1级遗留的三个问题均可以经过引入统一接口来解决。HTTP协议的七个标准方法是通过精心设计的,只要架构师的抽象能力够用,它们几乎能涵盖资源可能遇到的全部操做场景。REST的具体作法是:把不一样业务需求抽象为对资源的增长、修改、删除等操做来解决第一个问题;使用HTTP协议的Status Code,它能够涵盖大多数资源操做可能出现的异常,也能够自定义扩展,以此解决第二个问题;依靠HTTP Header中携带的额外认证、受权信息来解决第三个问题,这个在实战中并无体现。

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

GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
复制代码

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

HTTP/1.1 200 OK

[ {"id": 1, "start":"14:00", "end":"14:50", "doctor": "mjones"}, {"id": 2, "start":"16:00", "end":"16:50", "doctor": "mjones"} ] 复制代码

笔者仍然以为14:00的时间比较合适,因而进行预定确认,并提交了我的基本信息,用以建立预定,这是符合POST的语义的:

POST /schedules/1 HTTP/1.1

{"name": "zio", "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

[ { "id": 1, "start":"14:00", "end":"14:50", "doctor": "mjones", "links": [ {"rel": "confirm schedule", "href": "/schedule/1"} ] }, { "id": 2, "start":"16:00", "end":"16:50", "doctor": "mjones", "links": [ {"rel": "confirm schedule", "href": "/schedule/2"} ] } ] 复制代码

若是作到了第3级REST,那服务端的API和客户端也是彻底解耦的,此时若是你要调整服务数量,或者对同一个服务作API升级时将会变得很是简单。

对于第3级须要明确:若是客户端指的是移动端这类发布升级成本较高的场景,这样的设计确实友谊颇高;但若是是客户端是web前端,它们的发布成本和服务端无差,那么能够case by case的去看待,是否要把这类“href”信息维护在服务端,这样作是否有悖于“先后端分离”的思想。

推荐阅读

《凤凰架构·构建可靠的大型分布式系统》

相关文章
相关标签/搜索