简单来说:不管哪个“好”还是不“好”,RESTful API在很多实际项目中并不使用。因此真的做了项目,你可能会发现只能用HTTP+JSON来定义接口,无法严格遵守REST风格。

为什么说不实际呢?因为这个风格太理想化了,比方说:

  • REST要求要将接口以资源的形式呈现。但实际上,很多时候都不太可能将一些业务逻辑看作资源。即使强制这么干了,也会非常非常别扭。登录就是登录,而不是“创建一个session”;播放音乐就是播放,而不是“创建一个播放状态“。
我们之所以要定义接口,本身的动机是做一个抽象,把复杂性隐藏起来,而绝对不是把内部的实现细节给暴露出去。REST却反其道而行之,要求实现应该是“资源”并且这个实现细节要暴露在接口的形式上。

但一个好的接口设计就应该是简单、直观的,能够完全隐藏内部细节的,不管底层是不是资源,资源的组合还是别的什么架构。此外,让业务逻辑与接口表现一致,对系统的长期维护和演进都有极大的好处。
  • REST只提供了增删改查的基本语义,其他的语义基本上不管。比如批量添加,批量删除,修改一个资源的一部分字段。区分“物理删除”和“标记删除”等等。复杂的查询更加不显示,对于像筛选这类的场景,REST明显就是个渣。这里要表扬一下GraphQL(但GraphQL有其他的问题,在此不展开)
  • REST建议用HTTP的status code做错误码,以便于“统一”,实际上这非常难统一。各种业务的含义五花八门,抽象层次高低不齐,根本就无法满足需要。比如一个404到底是代表这个接口找不到,还是代表一个资源找不到。400表达请求有问题,但是我想提示用户“你登录手机号输入的格式不对“,还是“你登录手机号已经被占用了“。既然201表示“created”,为啥deleted和updated没有对应的status code,只能用200或者204(no content)?错误处理是web系统里最麻烦的,最需要细心细致的地方。REST风格在这里只能添乱。
  • web请求参数可能散布在url path、querystring、body、header。服务器端处理对此完全没有什么章法。客户端和服务器端的研发之间还是要做约定。
  • 在url path上的变量会对很多其他的工作带来不良影响。
比如监控,本来url可以作为一个接口的key统计次数/延迟,结果url里出了个变量,所以自动收集nginx的access log,自动做监控项目增加就没法弄了。
再比如,想对接口做流量控制的计数,本来url可以做key,因为有变量,就得多费点事才行。
  • 现实中接口要处理的真正的问题,REST基本上也没怎么管。比如认证、授权、流控、数据缓存(http的etag还起了点作用)、超时控制、数据压缩……。
  • REST有很多好的工具可以便利的生成对应的代码和文档,也容易形成规范。但问题是REST在实际的项目中并没有解决很多问题,也在很多时候不合用,因此产生的代码和文档也就没什么用,必须经过二次加工才能真的用起来。因此可以基于REST+你的业务场景定义一个你自己的规范。

REST的本意是基于一个架构的假设(资源化),定义了一组风格,并基于这个风格形成约定、工具和支持。思路不错。但是因为他的架构假设就是有问题的,因此后续一系列东西都建立在了一个不稳固的基础之上。同时,REST并没有解决太多的实际问题。

是,的确,有些时候,用REST完成CRUD已经能完成任务了。此时,用REST没有什么不好的。但是,现实当中,真正的业务领域一般都会比资源的CRUD复杂的多。这时REST“基本上没解决太多实际问题”的缺点就会体现出来。我所见到的大多数情况,是会形成一种REST-like形式的接口,像REST却又不限于REST。

为了REST,我看到了太多的人在争执到底是POST还是PUT,到底用querystring还是body,到底用200还是201,到底一个单词应该用单数还是复数,到底一个请求参数应该放在url path的中间一段还是最后一段…… 真正要做的事情本身反而没人关心。而一旦把争论给一个“REST专家”看,他的回答八成是“其实你还是不懂REST”...

我觉得人生不能这么糟蹋,你觉得呢?

---- 附一个现实当中接口的开发的方式

你可以总是从REST开始,如果你要开发的东西能被自然而然的想成是一个资源。然后通过相关的工具自动生成一些代码,把这个原型和你的合作者讨论一下。这是我能想到的REST能做的一件很好的事情——快速实验。

然后如果你想认真的往下做,就可以彻底忘记REST这件事。开始自己定义业务接口,尽量不要在url里加变量。尽量只用GET和POST,减少一些沟通上的混乱。对于每个接口,好好定义可能发生的业务错误,并与PM一起协商怎么处理这些错误。认真的考虑认证、授权、流控等机制,当你开发的是和钱相关的业务尤其要留意。

最后,本文并不是说“绝对不要用REST”,而是:如果你在实际工作中用REST有了困惑,不知道某个情况下REST此时的最佳实践是什么时,不要追求“真正的REST会怎么做”,不要被REST限制住。

----- 2018-12-19更新

感谢

的评论,基于他的评论,我写下我的感受。

如果看过REST最初的那篇论文Architectural Styles and the Design of Network-based Software Architectures就会发现,当时想设计的目标是解决互联网级别的信息共享和互操作问题。而我们的大量开发者工作的主要目标是“为业务系统实现一个满足功能(比如登录,交易……)/非功能需求(比如认证,性能,可扩展性……)的接口“。并且设计接口时会区分“给第三方用的开放接口”、“给UI开发定制的接口“和“内部使用的接口”等。这些接口的设计目标都和REST当初制定的目标有差别。其中最接近的,是“开放接口”。因此可以看到有些开放接口用REST实现还是很不错的,比如github的接口,AWS S3的接口等。

但是其他两类接口与REST关注的点完全不一样。比如面向UI的接口的就要满足UI需要。此时资源不资源不太重要,而是尽量用少的roundtrip去返回这个界面需要的所有数据。接口是按照加载的优先级,而不是“资源”做切分。比如第一屏的显示要尽量一个接口先给出来,后续异步加载的数据可以用其他接口慢慢出。为UI提供的接口往往被划归为“大前端“的一部分。

而内部的接口,越接近DB的,越容易用表来mapping到“资源“,但是内部的接口需要考虑到数据整合的需要。比如底层的用户数据分为A、B、C三类,但这3个数据因为服务隔离不能直接在DB做join。需求要按照A的某个条件做排序分页,但要注入B和C的数据。这时就需要B和C提供batch读取和app注入的相关逻辑。此外还有复杂的查询条件,可动态改变的输出字段等要求。REST的“资源”概念在这里能帮上的忙有限。这也是GrpahQL尝试解决的问题。

再有一类问题是用接口实现分布式一致性的业务问题。比如下单+支付+扣库存+加积分问题。这时接口的形式并不重要,而能够支持实现SAGA或者TCC才是最关键的。而整个业务对外的感觉实际上是创建一个“事务”。早期一本叫做Resftul Web Services的书描述Restful接口做这个事情的方案是:

  1. 调用接口创建一个事务的资源
  2. 拿着事务资源的id,调用步骤1接口,步骤2接口……
  3. 拿着事务资源的id,调用事务的commit接口

这种形式不仅臃肿,还把怎么做这件事的内部细节完全暴露到了调用方,造成了耦合。而我们一般常见的做法就是一个接口POST /doSomething,然后接口实现方内部维护事务,维护commit,rollback等细节。有的时候还需要添加一些异步回调。

简单总结下,写接口的目标各自不同。而REST的目标是“实现互联网级别的信息共享系统”,这个目标和大部分开发者要实现的目标完全不同,这就不难解释为何照搬REST去做另一个领域的事情可能会非常别扭。