2.2.2 RESTful的系统
如果你已经理解了上面这些概念,我们就可以开始讨论面向资源的编程思想与Fielding所提出的几个具体的软件架构设计原则了。Fielding认为,一套理想的、完全满足REST风格的系统应该满足以下六大原则。
1.客户端与服务端分离(Client-Server)
将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来,有助于提高用户界面的跨平台的可移植性,也越来越受到广大开发者所认可,以前完全基于服务端控制和渲染(如JSF这类)框架的实际用户已甚少,而在服务端进行界面控制(Controller),通过服务端或者客户端的模板渲染引擎来进行界面渲染的框架(如Struts、SpringMVC这类)也受到了颇大冲击。这一点与REST可能关系并不大,前端技术(从ES规范,到语言实现,再到前端框架等)在近年来的高速发展,使得前端表达能力大幅度加强才是真正的幕后推手。由于前端的日渐强势,现在还流行起由前端代码反过来驱动服务端进行渲染的SSR(Server-Side Rendering)技术,在Serverless、SEO等场景中已经占领了一席之地。
2.无状态(Stateless)
无状态是REST的一条核心原则,部分开发者在做服务接口规划时,觉得REST风格的服务怎么设计都感觉别扭,很可能的一个原因是服务端持有比较重的状态。REST希望服务端不用负责维护状态,每一次从客户端发送的请求中,应包括所有必要的上下文信息,会话信息也由客户端负责保存维护,服务端只依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。客户端承担状态维护职责以后,会产生一些新的问题,譬如身份认证、授权等可信问题,它们都应有针对性的解决方案[1]。
但必须承认的是,目前大多数系统都达不到这个要求,且越复杂、越大型的系统越是如此。服务端无状态可以在分布式计算中获得非常高价值的回报,但大型系统的上下文状态数量完全可能膨胀到客户端无法承受的程度,在服务端的内存、会话、数据库或者缓存等地方持有一定的状态成为一种事实上存在,并将长期存在、被广泛使用的主流方案。
3.可缓存(Cacheability)
无状态服务虽然提升了系统的可见性、可靠性和可伸缩性,但降低了系统的网络性。“降低网络性”的通俗解释是某个功能使用有状态的设计时只需要一次(或少量)请求就能完成,使用无状态的设计时则可能会需要多次请求,或者在请求中带有额外冗余的信息。为了缓解这个矛盾,REST希望软件系统能够如同万维网一样,允许客户端和中间的通信传递者(譬如代理)将部分服务端的应答缓存起来。当然,为了缓存能够正确地运作,服务端的应答中必须直接或者间接地表明本身是否可以进行缓存、可以缓存多长时间,以避免客户端在将来进行请求的时候得到过时的数据。运作良好的缓存机制可以减少客户端、服务端之间的交互,甚至有些场景中可以完全避免交互,这就进一步提高了性能。
4.分层系统(Layered System)
这里所指的分层并不是表示层、服务层、持久层这种意义上的分层,而是指客户端一般不需要知道是否直接连接到了最终的服务器,抑或连接到路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。该原则的典型应用是内容分发网络(Content Distribution Network,CDN)。如果你是通过网站浏览到这篇文章的话,你所发出的请求一般(假设你在中国境内的话)并不是直接访问位于GitHub Pages的源服务器,而是访问了位于国内的CDN服务器,但作为用户,你完全不需要感知到这一点。我们将在第4章讨论如何构建自动、可缓存的分层系统。
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等都属于典型的按需代码,蕴含着具体执行逻辑的代码是存放在服务端,只有当客户端请求了某个Java Applet之后,代码才会被传输并在客户端机器中运行,结束后通常也会随即在客户端中被销毁。将按需代码列为可选原则的原因并非是它特别难以达到,更多是出于必要性和性价比的实际考虑。
至此,REST中的主要概念与思想原则已经介绍完毕,我们再回过头来讨论本节开篇提出的REST与RPC在思想上的差异。REST的基本思想是面向资源来抽象问题,它与此前流行的编程思想——面向过程的编程在抽象主体上有本质的差别。在REST提出以前,人们设计分布式系统服务的唯一方案就只有RPC,RPC是将本地的方法调用思路迁移到远程方法调用上,开发者是围绕“远程方法”去设计两个系统间交互的,譬如CORBA、RMI、DCOM,等等。这样做的坏处不仅使“如何在异构系统间表示一个方法”“如何获得接口能够提供的方法清单”成为需要专门协议去解决的问题(RPC的三大基本问题之一),而且对于服务使用者来说,由于服务的每个方法都是完全独立的,他们必须逐个学习才能正确地使用这些方法。Google在“Google API Design Guide”[2]中曾经写下这样一段话。
额外知识
以前,人们面向方法去设计RPC API,譬如CORBA和DCOM,随着时间推移,接口与方法越来越多却又各不相同,开发人员必须了解每一个方法才能正确使用它们,这样既耗时又容易出错。
——Google API Deign Guide,2017
REST提出以资源为主体的服务设计风格,可以带来不少好处。(自然也有坏处,笔者将在下一节集中谈论REST的不足与争议。)
·降低服务接口的学习成本。统一接口是REST的重要标志,它将对资源的标准操作都映射到标准的HTTP方法上去,这些方法对于每个资源的用法都是一致的,语义都是类似的,不需要刻意去学习,更不需要有诸如IDL之类的协议存在。
·资源天然具有集合与层次结构。以方法为中心抽象的接口,由于方法是动词,逻辑上决定了每个接口都是互相独立的;但以资源为中心抽象的接口,由于资源是名词,天然就可以产生集合与层次结构。举个具体例子,假设一个商城用户中心的接口设计:用户资源会拥有多个不同的下级的资源,譬如若干条短消息资源、一份用户资料资源、一辆购物车资源,购物车中又会有自己的下级资源,譬如多本图书资源。你很容易在程序接口中构造出这些资源的集合关系与层次关系,而且这些关系是符合人们长期在单机或网络环境中管理数据的经验的。相信你不需要专门阅读接口说明书,就能轻易推断出获取用户icyfenix的购物车中的第2本书的REST接口应该表示为:
GET /users/icyfenix/cart/2
·REST绑定于HTTP协议。面向资源编程不是必须构筑在HTTP之上,但REST是,这是缺点,也是优点。因为HTTP本来就是面向资源设计的网络协议,纯粹只用HTTP(而不是SOAP over HTTP那样再构筑协议)带来的好处是无须考虑RPC中的Wire Protocol问题,REST将复用HTTP协议中已经定义的概念和相关基础支持来解决问题。HTTP协议已经有效运作了三十年,其相关的技术基础设施已是千锤百炼,无比成熟。而坏处自然是,当你想去考虑那些HTTP不提供的特性时,便会彻底束手无策。
以上列举了一些面向资源编程的优点,但笔者并非要证明它比面向过程、面向对象编程更优秀,是否选用REST的API设计风格,需要结合你的需求场景、你团队的设计和开发人员是否能够适应面向资源的思想来设计软件来权衡。在互联网中,面向资源进行网络传输是这三十年来HTTP协议精心培养出来的用户习惯,如果开发者能够适应REST这种不太符合人类思维习惯的抽象方式,使用REST匹配在HTTP基础上构建的互联网,相信在效率与扩展性方面会有可观的收益。
[1] 这部分内容可参见本书第5章。
[2] 地址:https://cloud.google.com/apis/design。