API设计模式漫谈(二) - 设计和选型
现代社会是个契约社会,生活中大大小小的事情都在和契约打交道。而契约在软件工程上最基本的体现就是函数。当一个函数被定义出来时:它便告诉它的使用者,你我之间应该如何合作。
比如说,一个函数可以是这样定义的:如果你传递给我类型为 X 的数据,我会返回给你类型为 Y 的结果,而且如果你传递相同的值进来,我给你相同的结果。这是 pure function,也是程序员最喜欢的契约形式,因为黑纸白字,清清楚楚,童叟无欺。
更普遍的情况是不那么纯粹的函数:如果你传递给我类型为 X 的数据,我会返回给你类型为 Y 的结果,当然,如果结果不存在,我会给你个 null;而且,如果我中间处理的过程出了差池,我会扔一颗或者若干颗叫「异常返回」的炸弹。此外,我不能保证你传递相同的值进来,都返回给你相同的结果(比如说数据库操作)。有副作用的函数尽管有诸多含混不清的地方,任然不失为一种契约。
函数级别的契约的所有当事人都是程序员,契约更新的影响面有限,所以遇到问题,原作者大笔一挥,新的契约就出现了。然而,新的契约出现并不意味着旧的契约的终止,只有当所有使用旧契约的地方都改用新契约时,我们才能安全地废除旧契约。就一个函数来说,如果是两人之间的事,更换契约也就是个把小时的事情;然而,像 Linux 这样复杂的系统,你改一个 listaddtail() 的接口,即使 Linus 不拍死你,我保证社区的口水也要淹死你。为啥?你触动了很多人的奶酪,大家围绕你原来契约构建的堡垒一夜之间均被瓦解了。
铺垫了这么多,其实就是想说明一件事:一旦你制定了一纸契约,你必须遵守它,且不要轻易改动它;使用契约的人越多,改动的代价越大。
那回到正文,REST API(以下简称 API)是什么?
REST API 就相当于上面提到的,基于服务器和客户端之间的契约。这就意味着一个中小规模的 API一旦发布,你基本失去了对其任意修改的权利,因为你无法期待脱离了掌控的使用者们能够像我们希望的那样,根据我们的调整能够步调一致地去大费周章修改自己基于API应用。
所以,即便你习惯于随心所欲地创建一个函数,然后在需要的时候重构之,当你做 API 时,你就会受到很多掣肘。因为你一但开始简单随意,就会给后续的维护和更新带来无穷无尽的痛苦。
所以我们需要好好进行设计 API 的接口。
我们知道,设计接口并不是一件轻松的活,我们要考虑:userability,simplicity,security,reliability 等等,设计好了还需要将其文档化。所以我们最好借助于工具的力量来设计 API,就像我们使用 visio 来设计网络拓扑或者画软件架构图一样。目前比较流行的 API 接口设计工具有 swagger,API blueprint 和 RAML。它们共同的特点是你可以很方便地描述 API 的输入输出,并生成交互式的 API 文档。所谓交互式 API 文档,是指用户在读 API 文档的时候,可以在线运行 API,获得结果。这样,API 的设计者就可以在还没有开始写代码的时候就反复推演 API 的结构,直到产生一个健壮的,清晰明了,可用性强的接口。
Swagger
swagger 是最早也是最成熟的 API 接口设计工具。它可以使用 json/yaml 来描述 API 的接口,使用 swagger 来设计和描述 API 有很多好处:API 的文档化,API 的接口的可视化,各种语言的客户端类库的自动生成,甚至服务端代码也能够自动生成。包括代码生成工具在内的完整而成熟的工具链是 swagger 的杀手锏,也是众多 API 制作者优先选择 swagger 的一个重要因素。我们看 swagger 的一个例子(instagram API):
paths:
/users/self/feed:
get:
tags:
- Users
description: See the authenticated user's feed.
parameters:
- name: count
in: query
description: Count of media to return.
type: integer
- name: max_id
in: query
description: Return media earlier than this max_id.s
type: integer
- name: min_id
in: query
description: Return media later than this min_id.
type: integer
responses:
200:
description: OK
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/definitions/Media'
这里定义了一个 API endpint /users/self/feed,他接受三个 querystring 参数,并在请求成功时(200)返回一个这样的对象:
{
"data": [...]
}
swagger 的缺点是太繁杂,撰写起来比较麻烦。但成熟稳定是它最大的优点。
API Blueprint
API Blueprint 更偏向 API 的文档化,所以它选择的描述语言是 markdown。三者之间 API blueprint 的描述语言可读性最强,更像是真的在撰写文档。然而,markdown 的强项不在表述语法,API 相关的内容用 markdown 描述不是很舒服,看别人写的文档很容易明白,自己写起来就会错漏百出。API blueprint 的工具链也是个薄弱环节,很多工具都没有或者不成熟。如果说工具的缺乏还可以通过时间来弥补,使用 markdown 这种对机器不太友好的定义语言来定义各种语法,则是 API blueprint 犯下的大错。因为,对比三者的语法,它们的学习曲线都很长,遗忘指数都很高(不是经常用),指望程序员来写还不如指望机器帮你生成。而机器生成强语法结构的 json / yaml 会相对简单。
所以,权衡之下,个人比较不倾向于使用 API blueprint。
RAML
RAML 使用 yaml 来描述 API。它被设计地很灵活,很容易把描述分解到多个文件里然后相互引用。就描述语言来说,RAML 像是一个蓬勃向上的少年,精明而干练;而 swagger 已经垂垂老矣,冗长而乏味。我一开始在 RAML 和 swagger 两者间左右摇摆,写了不少测试代码,如果不是 swagger 的工具链过于吸引人,而 RAML 1.0 版本还处在 beta 阶段,个人可能会改变选择 RAML。
小结
总而言之,API 写得再好,没有一个与之对应的契约,是万万不行的 —— 没有文档描述的 API 就如同没有说明书的产品。文档和代码,如同泉水干涸之后相呴以湿、相濡以沫的鱼儿,谁也离不开谁