2.2.4 不足与争议
以下是笔者所见过的关于REST能否在实践中真正良好应用的部分争议问题,笔者将自己的观点总结如下。
1)面向资源的编程思想只适合做CRUD,面向过程、面向对象编程才能处理真正复杂的业务逻辑。
这是遇到最多的一个问题。HTTP的四个最基础的命令POST、GET、PUT和DELETE很容易让人直接联想到CRUD操作,以至于在脑海中自然产生了直接的对应。REST所能涵盖的范围当然远不止于此,不过要说POST、GET、PUT和DELETE对应于CRUD其实也没什么不对,只是这个CRUD必须泛化去理解。这些命令涵盖了信息在客户端与服务端之间流动的几种主要方式,所有基于网络的操作逻辑,都可以对应到信息在服务端与客户端之间如何流动来理解,有的场景比较直观,而有的场景则可能比较抽象。
针对那些比较抽象的场景,如果真不能把HTTP方法映射为资源的所需操作,REST也并非刻板的教条,用户是可以使用自定义方法的,按Google推荐的REST API风格,自定义方法应该放在资源路径末尾,嵌入冒号加自定义动词的后缀。譬如,可以把删除操作映射到标准DELETE方法上,如果还要提供一个恢复删除的API,那它可能会被设计为:
POST /user/user_id/cart/book_id:undelete
如果你不想使用自定义方法,那就设计一个回收站的资源,在那里保留还能被恢复的商品,将恢复删除视为对该资源某个状态值的修改,映射到PUT或者PATCH方法上,这也是一种完全可行的设计。
最后,笔者再重复一遍,面向资源的编程思想与另外两种主流编程思想只是抽象问题时所处的立场不同,只有选择不同,没有高下之分。
·面向过程编程时,为什么要以算法和处理过程为中心,输入数据,输出结果?当然是为了符合计算机世界中主流的交互方式。
·面向对象编程时,为什么要将数据和行为统一起来、封装成对象?当然是为了符合现实世界的主流的交互方式。
·面向资源编程时,为什么要将数据(资源)作为抽象的主体,把行为看作统一的接口?当然是为了符合网络世界的主流的交互方式。
2)REST与HTTP完全绑定,不适合应用于要求高性能传输的场景中。
笔者很大程度上赞同此观点,但并不认为这是REST的缺陷,正如锤子不能当扳手用并不是锤子的质量有问题。面向资源编程与协议无关,但是REST(特指Fielding论文中所定义的REST,而不是泛指面向资源的思想)的确依赖着HTTP协议的标准方法、状态码、协议头等各个方面。HTTP并不是传输层协议,它是应用层协议,如果仅将HTTP用于传输是不恰当的。对于需要直接控制传输,如二进制细节、编码形式、报文格式、连接方式等细节的场景,REST确实不合适,这些场景往往存在于服务集群的内部节点之间,这也是之前笔者曾提及的,REST和RPC尽管应用确有所重合,但重合范围的大小就是见仁见智的事情。
3)REST不利于事务支持。
这个问题首先要看你怎么看待“事务(Transaction)”这个概念。如果“事务”指的是数据库那种狭义的刚性ACID事务,那除非完全不持有状态,否则分布式系统本身与此就是有矛盾的(CAP不可兼得),这是分布式的问题而不是REST的问题。如果“事务”是指通过服务协议或架构,在分布式服务中,获得对多个数据同时提交的统一协调能力(2PC/3PC),譬如WS-AtomicTransaction、WS-Coordination这样的功能性协议,REST是不支持的,假如你理解了这样做的代价,仍坚持要这样做的话,Web Service是比较好的选择。如果“事务”只是指希望保障数据的最终一致性,说明你已经放弃刚性事务了,这才是分布式系统中的正常交互方式,使用REST肯定不会有什么阻碍,更谈不上“不利于”。当然,对此REST也并没有什么帮助,这完全取决于你的系统的事务设计,我们在第3章中再详细讨论。
4)REST没有传输可靠性支持。
是的,并没有。在HTTP中发送一个请求,你通常会收到一个与之相对的响应,譬如HTTP/1.1 200 OK或者HTTP/1.1 404 Not Found等。但如果你没有收到任何响应,那就无法确定消息是没有发送出去,抑或是没有从服务端返回,这其中的关键差别是服务端是否被触发了某些处理?应对传输可靠性最简单粗暴的做法是把消息再重发一遍。这种简单处理能够成立的前提是服务应具有幂等性(Idempotency),即服务被重复执行多次的效果与执行一次是相等的。HTTP协议要求GET、PUT和DELETE应具有幂等性,我们把REST服务映射到这些方法时,也应当保证幂等性。对于POST方法,曾经有过一些专门的提案,如POE(POST Once Exactly),但并未得到IETF的认可。对于POST的重复提交,浏览器会出现相应警告,如Chrome中“确认重新提交表单”的提示,对于服务端,就应该做预校验,如果发现可能重复,则返回HTTP/1.1 425 Too Early。另外,Web Service中有WS-ReliableMessaging功能协议用于支持消息可靠投递。类似的,由于REST没有采用额外的Wire Protocol,所以除了事务、可靠传输这些功能以外,一定还可以在WS-*协议中找到很多REST不支持的特性。
5)REST缺乏对资源进行“部分”和“批量”处理的能力。
这个观点笔者是认同的,这很可能是未来面向资源的思想和API设计风格的发展方向。REST开创了面向资源的服务风格,但它并不完美。以HTTP协议为基础给REST带来了极大的便捷(不需要额外协议,不需要重复解决一堆基础网络问题,等等),但也使HTTP本身成了束缚REST的无形牢笼。这里仍通过具体例子来解释REST这方面的局限性。譬如你仅仅想获得某个用户的姓名,如果是RPC风格,可以设计一个“getUsernameById”的服务,返回一个字符串,尽管这种服务的通用性实在称不上“设计”二字,但确实可以工作;而如果是REST风格,你将向服务端请求整个用户对象,然后丢弃掉返回结果中该用户除用户名外的其他属性,这便是一种过度获取(Overfetching)。REST的应对手段是通过位于中间节点或客户端的缓存来解决这种问题,但此缺陷的本质是由于HTTP协议完全没有对请求资源的结构化描述能力(但有非结构化的部分内容获取能力,即今天多用于断点续传的Range Header),所以返回资源的哪些内容、以什么数据类型返回等,都不可能得到协议层面的支持,要做就只能自己在GET方法的Endpoint上设计各种参数来实现。另外一方面,与此相对的缺陷是对资源的批量操作的支持,有时候我们不得不为此而专门设计一些抽象的资源才能应对。譬如你准备给某个用户的名字增加一个“VIP”前缀,提交一个PUT请求修改这个用户的名称即可,而你要给1000个用户加VIP前缀时,如果真的去调用1000次PUT,浏览器会回应HTTP/1.1 429 Too Many Requests。此时,你就不得不先创建一个任务资源(如名为“VIP-Modify-Task”),把1000个用户的ID交给这个任务,然后驱动任务进入执行状态。又譬如你去网店买东西,下单、冻结库存、支付、加积分、扣减库存这一系列步骤会涉及多个资源的变化,你可能面临不得不创建一种“事务”的抽象资源,或者用某种具体的资源(譬如“结算单”)贯穿这个过程的始终,每次操作其他资源时都带着事务或者结算单的ID。HTTP协议由于本身的无状态性,会相对不适合(并非不能够)处理这类业务场景。
目前,一种理论上较优秀的可以解决以上这几类问题的方案是GraphQL,它是由Facebook提出并开源的一种面向资源API的数据查询语言,如同SQL一样,挂了个“查询语言”的名字,但其实CRUD都做。比起依赖HTTP无协议的REST,GraphQL可以说是另一种有协议的、更彻底地面向资源的服务方式。然而凡事都有两面性,离开了HTTP,它又面临几乎所有RPC框架所遇到的那个如何推广交互接口的问题。