下午三点,我在Postman里盯着一个订单接口的返回体,内心有点崩溃。这个接口返回了订单,还顺带把用户、优惠券、物流、分销商的信息全揉进了JSON里,嵌套三层,字段一共四十多个。前端同事说这个接口“很香”,因为一个请求全拿到了;但后端同事每次改动都要拉一个群,因为没人知道谁在使用某个“多余”字段。更讽刺的是,这个接口的路径叫 /orders/{id},标准RESTful,看起来干干净净的。
其实三年前,我是RESTful最狂热的布道者。当时团队内部微服务刚起步,我拍桌子要求所有接口必须遵循资源导向,/users/{id},/orders/{id}/items,动词一律不许出现在URL里。我们还参考了所谓的Richardson成熟度模型,把那些用/order/create的接口嘲笑成“土包子”。现在回头看看,那时候的自己就像刚健身两个月就到处教别人打蛋白粉的菜鸟,以为自己懂了很多,其实只知道表面。
为什么会有这种转变?因为我发现,RESTful的核心假设“资源是稳定的”在我们的业务里根本不成立。订单不是静态资源,它是一个不断变形的状态机:待支付、已支付、已发货、已完成,每个状态下的字段和操作都不一样。REST里表达“取消订单”这样的动作时,所有解决方案都透着别扭。你可以POST /orders/{id}/cancel,也可以把cancel当成子资源,但本质上这就是一个方法调用。对内部服务来说,调用方根本不想关心“资源长什么样”,他们只想“取消这个订单”,用orders.cancel(id) 不是更直接吗?我一度很纠结,觉得自己背叛了什么。
后来我们决定在实践中做一个混合:内部服务间的写操作全部改用gRPC,定义一个订单服务,里面有 Cancel、Pay、MarkShipped 这些显式方法;读操作保留REST,因为缓存和查询比较友好。这个双轨制一开始被很多人骂,说一个服务两套风格,怎么维护?确实,最初很混乱,protobuf文件升级时差点搞出版本冲突。但磨合了一段时间后,大家反而找到了节奏。外部客户机需要的是结构化的数据展示,所以REST表现得好;内部业务系统关心的是业务动作的结果,所以RPC更贴合心智模型。这个发现不算颠覆,但对我来说,是难得走出来的一次自我否定。明白了这一点,很多API设计上的执念就放下了。
有一次会议,同事提出一个方案:把取消订单直接写成 POST /order/{id}/cancel,并且要求带上原因,我当时的第一反应是“这不RESTful”,但我硬生生把这句话咽了回去。仔细想想,这个路径清楚明白,语义也强,调用方根本不用查文档。我们唯一需要确保的是它不会带偏其他接口的设计风格。后来我甚至允许了 /order/{id}/assign_to/{userId} 这种写法,这在REST纯化论者眼里可能算大逆不道,但实际效果确实好。我渐渐觉得,真正的接口设计不应该是风格的服从,而应该是帮助调用者减少思考成本。如果必须加一个额外的“解释型备注”才能让开发者理解这个接口的意图,那这就是设计的失职。
现在,我们的新项目里,风格反而变得自由了。我们不再说“这是REST”或“这是RPC”,我们只说“这个接口的目标是让调用方输入最少,让返回方的变化可预测”。这个原则没写进文档,但大家都能心领神会。当然,这不意味着我们回到随手写接口的原始时代,差评还是会有,只是评价标准不再是“合不合规范”,而是“踏不踏实”。我不知道这是不是所谓的“全新独立观点”,但这些经历让我明白,API开发的真正深度不在于你掌握了多少协议,而在于你能在多大程度上容忍不确定性,并且在乱糟糟的现实里做出让团队省心的决定。