再谈 API 的撰写 - 契约

现代社会是个契约社会,生活中大大小小的事情都在和契约打交道。去奥莱买件衣服,一纸小票,便是你跟商家的契约:你花钱买到了产品,产品的问题商家会承诺处理(退换货)。如果你用信用卡交款,你和银行之间,银行和商家之间又达成了一系列契约:银行会在未来的某个时刻扣除你的 credit,这 credit 你需要用钱来赎回;银行同时欠下商户几乎等值的 credit,这 credit 会在月末付给商户。

契约

契约在软件上最基本的体现就是函数。当一个函数被定义出来时:它告诉它的使用者,你我之间应该如何合作。

比如说,一个函数可以是这样定义的:如果你传递给我类型为 X 的数据,我会返回给你类型为 Y 的结果,而且如果你传递相同的值进来,我给你相同的结果。这是 pure function,也是程序员最喜欢的契约形式,因为黑纸白字,清清楚楚,童叟无欺。

更普遍的情况是不那么纯粹的函数:如果你传递给我类型为 X 的数据,我会返回给你类型为 Y 的结果,当然,如果结果不存在,我会给你个 null,爱谁谁;而且,如果我中间处理的过程出了差池,我会扔一颗或者若干颗叫「异常」的炸弹,咱俩要么哥俩好(你处理异常),要么同归于尽(不处理)。此外,我不能保证你传递相同的值进来,都返回给你相同的结果(比如说数据库操作)。有副作用的函数尽管有诸多含混不清的地方,任然不失为一种契约。

函数级别的契约的所有当事人都是程序员,契约更新的影响面有限,所以遇到问题,姐弟俩一商量,改!新的契约就出现了。然而,新的契约出现并不意味着旧的契约的终止,只有当所有使用旧契约的地方都改用新契约时,我们才能安全地废除旧契约。就一个函数来说,如果是两人之间的事,更换契约也就是个把小时的事情;然而,像 linux 这样复杂的系统,你改一个 list_add_tail() 的接口试试(假设你有权限),即使 Linus 不拍死你,我保证社区的口水也要淹死你。为啥?你触动了很多人的奶酪。

铺垫了这么多,就是想说明一件事:一旦你制定了一纸契约,你必须遵守它,且不要轻易改动它;使用契约的人越多,改动的代价越大。我们定义一个陈氏指数 CEI —— 契约使用指数(Contract Employ Index),每百万使用者记为 1。CEI 越高,表明使用者越多,同样的,改动的代价就越大。

REST API(以下凡提到 API,都指 REST API)是什么?REST API 是服务器和客户端之间的契约。这就意味着一个中小规模的 API,其 CEI 起码在 0.1 以上。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):

这里定义了一个 API endpint /users/self/feed,他接受三个 querystring 参数,并在请求成功时(200)返回一个这样的对象:

{    "data": [...]
}

swagger 的缺点是太繁杂,撰写起来很麻烦。不过,这不是什么大问题,所以我最终选择了 swagger。

API Blueprint

API Blueprint 更偏向 API 的文档化,所以它选择的描述语言是 markdown。三者之间 API blueprint 的描述语言可读性最强,更像是真的在撰写文档。

然而,markdown 的强项不在表述语法,validation 相关的内容用 markdown 描述不是很舒服,看别人写的文档很容易明白,自己写起来就会错漏百出。API blueprint 的工具链也是个薄弱环节,很多工具都没有或者不成熟。如果说工具的缺乏还可以通过时间来弥补,使用 markdown 这种对机器不太友好的定义语言来定义各种语法,则是 API blueprint 犯下的大错。因为,对比三者的语法,它们的学习曲线都很长,遗忘指数都很高(不是经常用),指望程序员来写还不如指望机器帮你生成。而机器生成强语法结构的 json / yaml 相对简单,生成弱语法结构的 markdown 则要填不少坑。

所以,权衡之下,三者之间,我最先淘汰的是 API blueprint。

RAML

RAML 使用 yaml 来描述 API。它被设计地很灵活,很容易把描述分解到多个文件里然后相互引用。

就描述语言来说,RAML 像是一个蓬勃向上的少年,精明而干练;而 swagger 已经垂垂老矣,冗长而乏味。我一开始在 RAML 和 swagger 两者间左右摇摆,写了不少测试代码,如果不是 swagger 的工具链过于吸引人,而 RAML 1.0 版本还处在 beta 阶段,我可能会最终选择 RAML。

契约和实现合二为一

如果我们从 swagger 出发,设计好 API 的接口,然后再用某种语言实现这个接口,显得有些累赘,日后改接口时,得改代码;改代码后如果变动了接口,还得回头改 swagger 的声明,这样太累心,迟早会出不一致的问题。一旦不一致,之前所做的所有努力就泡汤了:你提供了契约,却没有按照契约去行事。

swagger 考虑到了这一点,它能帮你生成客户端的 SDK 和服务器端的 stub。客户端的 SDK 还好,客户端的其他代码都是单向调用 SDK,重新生成并不会影响太大;服务端的代码需要 API 实现者实现,即便生成了 stub,肯定是要修改和添加功能的,所以如果修改 swagger 文档后,你不能再重新生成服务器端的 stub 了,因为这样有可能覆盖掉你已经修改的代码。所以大家使用 swagger 的方式基本都是服务器这端完全自己写,不用 stub。这样的话,上述的问题依旧存在。

另一种解决方案是通过 API 代码反向生成 swagger 文档。乍一看这似乎违背了 API 描述语言的初衷:我们竟然在没开始设计之前,就开始写代码了。

不必过虑。我们可以把代码的结构调整地更贴近描述语言。你可以先撰写代码把 API 的输入输出定义清楚,然后通过这个定义来生成 swagger 文档,在 swagger-ui 里面调试和验证;当借口设计符合期望后,再完成具体的实现。比如说这样用代码描述 API:

抛开 action 是什么不提,这段代码几乎和你用 YAML 描述 API 的接口如出一辙(这里缺了描述 response 的内容)。我们可以使用它生成 swagger 文档来验证:

通过代码反向生成 swagger 文档的好处是代码和文档总是一致的,API 的实现和契约相互印证;缺点是程序员看见代码就像看见九天仙女一样,眼迷心荡,刚定义好接口,还未细思,就忙不迭地去实现了。

尾声

这个系列竟然写出了五篇文章,大大出乎我的意料。韩非子说:善张网者,引其纲。不一一摄万目而后得。做 API 是个提纲携领的活,你要从纷扰的「万目」中找到那根系网的大绳,牵动之,网就搭好了。这根绳,不消说,就是我在 再谈 API 的撰写 - 架构 那篇文章中所述的 Pipeline。以此为纲,自顶向下,层层递进,你便豁然开朗。然而,API 写得再好,没有一个与之对应的契约,是万万不行的 —— 没有文档描述的 API 就如同没有说明书的产品。文档和代码,如同泉水干涸之后相呴以湿,相濡以沫的鱼儿,谁也离不开谁。能否防止它们随着时间的流逝功能的增加而相忘于江湖,是考量每个程序员能力和操守的一杆秤。

原文发布于微信公众号 - 程序人生(programmer_life)

原文发表时间:2016-03-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Kirito的技术分享

给初中级JAVA准备的面试题

笔者作为一个今年刚毕业的初级JAVA,根据群里水友的讨论,也结合自己刚毕业时的一些面经,加上近期一点点在公司面试别人的经验,总结了如下的常见面试问题,适用于初级...

5458
来自专栏程序人生

谈谈编译和运行

[作者按] 今天 hacker news 爆炸性的新闻是我们敬爱的葛老头:Andy Grove 去了。70后,80后大多听过这个响当当的名字,也听过(或者读过)...

4009
来自专栏涤生的博客

天池中间件大赛Golang版Service Mesh思路分享

这次天池中间件性能大赛初赛和复赛的成绩都正好是第五名,出乎意料的是作为Golang是这次比赛的“稀缺物种”,这次在前十名中我也是侥幸存活在C大佬和Java大佬的...

1544
来自专栏大数据挖掘DT机器学习

如何用R语言从网上读取多样格式数据

第一部分:数据信息 生活中,我们面临着各种各样的数据:比如你的成绩单,比如公司的财务报表,比如朋友圈的一些状态,比如微信里的一段语音……我们生活的大数据时代的一...

3305
来自专栏架构师之旅

3个面试中遇到的问题《JAVA面试题》

面试官:“一个http 请求,接受json数组,数组内容是id,返回用户信息,在测试上是ok的,到预生产就报错了,可能是什么问题?” 我想了想说:“代码一致吗?...

7735
来自专栏牛客网

2018腾讯、美团C++后台研发实习生面经

2720
来自专栏大数据和云计算技术

对象存储入门

10.5.3 对象接口 对象存储系统(Object-BasedStorage System)是综合了NAS和SAN的优点,同时具有SAN的高速直接访问和NAS...

6914
来自专栏Golang语言社区

【Go 语言社区】在 Go 语言中,如何正确的使用并发

Glyph Lefkowitz最近写了一篇启蒙文章,其中他详细的说明了一些关于开发高并发软件的挑战,如果你开发软件但是没有阅读这篇问题,那么我建议你阅读一篇。这...

3559
来自专栏恰同学骚年

Unity3D游戏开发初探—1.跨平台的游戏引擎让.NET程序员新生

  Unity是由Unity Technologies开发的一个让轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,...

1173
来自专栏CSDN技术头条

PHP 7.0.0正式版来了

PHP团队宣布PHP 7.0.0上线了。这是PHP7新系列开始的标志。 以下为官方发布稿: PHP 7.0.0 带来了新版本的Zend引擎,不仅如此,还有许多新...

2309

扫码关注云+社区

领取腾讯云代金券