RESTful Hypermedia Contract(RHC) 是HATEOAS原则的工业化扩展规范。它通过在RESTful API响应中嵌入超媒体链接(Hypermedia Links)、操作元数据(Operation Metadata)和参数契约(Parameter Contract),使API响应成为一份"自包含的运行时接口说明书"。客户端通过解析RHC响应,可动态发现资源关联、理解操作约束、适配参数结构,无需依赖外部文档或硬编码URI,实现API演进的零破坏性升级。

1 前言

Representational State Transfer (REST) 自从2000年由Roy Fielding博士提出来,得到了广泛的接受和支持,已经成为了事实上API开发的标准方案。其 HATEOAS(Hypermedia as the Engine of Application State,超媒体作为应用状态引擎)则是对于资源的导航和发现做出了详尽的定义。

时过26年后,这套规范依然应用广泛。但是在一些细节上,已经难免落后于现代的需求了。 比如其核心约束 HATEOAS, 作为资源的自发现和导航,实际上用处非常的有限。 一些严谨的系统中,会遵循HATEOAS在返回中增加相关的导航链接。但是大部分系统基本都忽略了这个机制。即便是尊重返回导航链接的API实现中,返回的链接也非常有局限性,缺乏通用性。

本规范试图在这个基础上,描述一套资源的完整自发现的规则, 使得前端可以通过API,就可以获取完整且详尽的资源列表和资源定义。由此前端完全可以采取一个通用的不变的固定框架,来处理任意的REST 资源定义。 符合本规范的情况下,对于后端来说,可以随时调整API的内容;对于前端来说,则可以不做任何变更,自动适应后台的最新的API.

需要注意的是,超媒体契约(RHC)不适合所有的场景。超媒体契约的理想目标是后端API可以任意的,快速的迭代,变更,于此同时前端无需做任何修改即可体现后端的api的效果。 解决的是前后端耦合过紧导致开发流程冗长,缓慢的问题。 在这个场景下,API的调用方,称为前端。也就是一个最终的端点。不会有后续的业务依赖。 对于非超媒体契约所适用的场景,举例来说,后端A提供的API的调用方是后端B。 那么后端B的相关业务,甚至他提供的API,都可能依赖于后端A的API. 这种情况下,完全自动的API自发现,自适应,可能会有风险蔓延,业务断层等的危险。 这种情况下就比较适用文档化,固化不变的API定义。

超媒体契约比较经典的应用场景是管理后台。 尤其是硬件类,后台服务类的运维管理。 这种场景下,前端是比较确定的网页应用,不存在下一步的业务依赖。

2 核心思想

REST 将一切用途,统一为资源。所有的操作,都是针对资源的标准化操作。 这种高层级的抽象使得所有不同类型的应用,其解决思路都非常的清晰和一致。

超媒体契约(RHC)则是在这一层上作进一步的拆分和定义。

RHC将REST中单一的资源概念,细分为 空间和实体。 空间是容器,实体是原有的资源概念。 如果类比的话,可以理解为空间是目录, 实体是文件。而原有的REST只定义了全路径的文件这一个概念。

具体来说, 在REST中,一个API的端点可能是 /api/v1/erm/account 。 这个全路径作为一个完整且单一的端点。 但是从 超媒体契约(RHC)看来,这个全路径里, /api, /api/v1, /api/v1/erm 都是空间,都有含义。

在超媒体契约RHC中, 实体依然是原有的概念。(这意味着完整的兼容REST)。 但是对于空间, 超媒体契约 将他定义为包含子空间和实体的容器。 且支持一个统一的枚举操作。 换而言之, 你可以直接访问 空间API /api/v1/erm/, 而他也会统一的返回该空间下的所有子空间和实体。比如 user, account, order, flow, 以及 finance (namespace), salary (namespace), 等等。

额外补充一点的是,超媒体契约需要一个根的概念。可以理解为根目录。从这个根开始,可以通过读取空间来获得所有下一级的空间和资源。 就像从根目录开始,你可以用 ls, cd 来发现并查看任意的文件一样。

因此超媒体契约中,需要明确的定义根。到底是 /api, 还是/api/v1, 还是 /api/v1/erm。 需要明确的指定才能开始发现所有的实体资源。 而对于REST来说,只需要一个完整的路径/api/v1/erm/account, 至于从哪儿开始的完全不关系。

3 概念定义

3.1 根或根空间

超媒体契约的起点位置。前端从这儿开始发现所有的资源。 根必然是空间,因此名称就叫根空间。 理解上可以视为根目录。

3.2 空间或空间资源

空间是实体的容器。 空间本身只具备一个统一的操作,就是枚举其包含的内容。内容可能是其他的空间,也可能是实体。 理解上可以视为文件夹,目录。

空间的定义使得REST可以更加的层次化。 通过空间将API分组,概念上更加清晰一些。

3.3 实体或者实体资源

等同于REST的资源的概念。

3.4 资源类型和执行操作

RFC2616, 7231, 5789, 9110中定义的HTTP方法共八种:GET,POST,PUT/PATCH,DELETE,HEAD,OPTIONS,TRACE,CONNECT.

REST没有明确的定义方法,但是基于他将万物视为资源的思想,绝大部分的研发设计人员都会将它往CRUD上靠,并对齐到POST,GET,PUT/PATCH,DELETE几个方法上。 这种对齐的方法,不仅简单可靠,事实上也能解决99%以上的问题。

但是仍存在一些很拗口,很生硬的资源化例子。比如登录/注销,比如转账, 比如上车,下车。 这些操作很难用合理、可理解、容易有基本共识的资源去做映射。 当前绝大部分系统中最这类的处理,处于尊重REST的角度,都是生造了一个资源去映射。 从而从事实上违背了REST要达成简单的思想共识的目标。

超媒体契约提出一个新的规范建议,即新增一种基本的方法类型为 EXECUTABLE。 也就是某些业务功能,他就是操作,作为资源去抽象反而更难理解。那么久单独的定义一个类型:执行。

登录/注销,转账等等,就是本身的 login, logout,transfer。 语义明确清晰。只是其类型明确的定义为 executable。 不能用CRUD的方法去操作他。唯一的操作就是直接调用执行。

这个思路显然是简单清晰的,但是绝大部分人在一开始这么想时,总是犹豫或者存疑,这是否RESTful?

为此我们可以从概念上澄清定义一下: 如果万物都是资源,资源就可以是文件。熟悉操作系统底层机制的朋友都知道,万物皆文件(句柄),是现代操作系统的无上利器。 既然万物都可以是文件,资源又有什么可以例外的呢?

而文件呢,有一个基本的属性,就是是否可执行。 可执行的,就是一个命令;不可执行的,才是什么图片啊,文本啊,words啊,ppt啊之类的。也就是在所有的文件类型之前,先分一个大类:是否可执行。 windows 下是用扩展名来区分的, linux中更纯粹一下。直接单独给另一个标记位x。

我们只需要将REST的资源,按照文件的概念,从类型上首先加一个是否可执行的标记,就可以完整的将动作/操作直接映射为资源,而不必费心拗口的单独去做资源化的抽象。

由此,我们只需要将REST的资源(RHC的实体资源),增加一个类型的定义,并且大类就是是否可执行,就可以完美的解决类似 登录/注销这类的RESTful语义混淆的问题了。

也可以认为CRUD用作业务处理是不够的,需要加一个X,CURDX才能完整的表征业务。