雷锋网按:本文作者朱赟,Airbnb 资深程序媛。由授权雷锋网发布。
经历过一个小公司成长为大公司,可能你也遇到过这样的情形:当你看到一行代码,觉得不是那么值得推敲。于是你用 git blame 寻找它的主人,赫然发现,居然原作者是那位如今早已不写 code 的 CTO 或者 VP 或者 Director 了。
然后一个偶然的机会,你跟他聊天提到这件事,他会很自豪地给你讲个故事:“哦,那时候,我们必须一天做出这个产品特性。当时也就我一个程序员吧,可能 Tom 也在。一天的时间,这是当时能做出最好的方案了。” 说完,他便陷入了对美好时光的遐思……
你可能也听说过类似这样的故事:有一天,你的 CTO 突发奇想,觉得自己其实还可以写一些代码。于是华丽丽提交了一段代码。大家一看,很激动啊,于是很多人咔咔咔开始在 PR 上给 comments。你的 CTO 一看:靠,几十条comments……现在这个代码要这么写啊?这么麻烦啊?于是跟一个工程师说,“你把 comments address 下,然后 merge 吧。” 然后就开开心心地自己该干啥干啥去了。
哦,有点离题了。
其实这两个故事,想说的是:一个公司早期的代码因为各种历史原因,可能不是那么完美,但是在特定的时候,那就是最好的方案。随着时间的消逝,功能不断叠加,代码架构不断优化。系统可能会经历一些变复杂、再简化的迭代过程。然后某一天,代码会面目全非,最初的主人也已经不认识自己当初的作品了。
API 的设计和实现尤为如此。一套成熟的 API,很多时候都是不断演化迭代出来的,很少有 API 的设计和实现从最开始就是完美无瑕疵的,说说自己做 API 的一些体会吧。
先从 API 的 signiture 说起吧。也就是 API request 和 response 支持哪些格式、哪些参数。
首先,做过 API 的人都知道,一个上线使用的 API 再想改它的 signiture,通常由于 compatibility 的原因,后期再想改,都是格外痛苦不堪的。因此,API signiture 设计初期,一定要反复推敲再推敲,尽量避免上线后的改动。
而除了一些基本的 RESTful 原则外,Signiture 的定义很多时候是对业务逻辑的抽象过程。一个系统的业务逻辑可能错综复杂,因此 API 设计的时候就应该做到用最简洁直观的格式去支持所有的需求。这其实往往是 API 设计中相对立的两面。有时候我们为了支持某一个功能,似乎不得不增加一个很违反设计的接口;而有时候我们为了保证 API 绝对规范,似乎又不得不放弃对某一些功能的直接支持,因此功能只能通过迭代调用或 client 端预处理来实现。
而这种设计上的取舍,通常只有列出所有可行的方案,从简单的设计到繁杂的设计,然后通过分析各种使用实例的频率和使用某种设计时的复杂度,从实际的系统需求入手,尽可能让常用的功能得到最简单直接的支持,而一定程度上 “牺牲” 一些极少用到的功能。反复推敲系统场景,尽可能取得一个合理的折衷。
在这个取折衷的过程中,始终保证下面的一些基本原则被满足,例如:
保证 API 100% RESTful。RESTful 的核心是 everything is a “resource”,所有的 action 和接口,都应该是相应 resource 上的 CRUD 操作。如果脱离这种设计模式,一定要再三考虑是不是必要?有没有其他方案可以避免。
在 request 和 response 中,都应该尽可能的保持参数的结构化。如果是一个 hash,就传一个 hash(不要传 hash.to_string)。API 的 serialization / deserialization 会将其自动序列化成字符串。多语言之间的 API,比如 Ruby,Java,C# 之间的调用,通常都是在 serialization / deserialization 中完成不同语言间类型的转换。
Authentication 和 Security 的考虑,应该始终放在首位。保证对特定的用户永远只 expose 相关的接口和权限。Authentication 可能是使用证书和白名单,也可能是通过用户登陆的 credentials 生成的验证 token,或者 session / cookie 等方式来处理。此外,所有的 API 层的 logging,应该保证不要 log 任何 sensitive 的信息。
API 本身应该是 client 无关的。也就是说,一个 API 对 request 的处理尽可能避免对 client 是 mobile 还是 web 等的考虑。Client 相关的 response 格式,不应该在 API 中实现。而所有的 client 无关的计算和处理,又应该尽可能的在 server 端统一处理。以提高性能和一致性。
尽可能让 API 是 Idempotent(幂等)的。这有好几个不同层次的含义。举几个例子:同一个 request 发一遍和发两遍是不是能够保证相同结果?Request 失败后重发和第一次发是不是能保证相同结果?当然具体的做法还是要看应用场景。
另外,每个语言都已经提供了很好的 API 框架。设计前先多了解这些框架。
为什么说多了解呢?如果你是一个小团队,可能多方比较后,选一个合适的框架入手,适当调整,比从零开始造轮子要好。但实际中,很多公司由于各自业务逻辑的特殊需求,最终都会有一套自己的定制方案。
而评估一个框架,可以从以下几个方面考虑:
对访问权限的统一控制
自动测试的支持
对 request 和 response 的 formatting,以及 serialization 和 deseialization 的支持
对 logging 和 logging filtering 的支持
对自动文档生成的支持
实际实现的架构以及性能的考虑
最后,如何处理设计中的一些对立面。
除了上文中提到的,接口简洁和功能繁复之间偶尔存在的对立,API 设计和实现中还有很多别的对立和取舍。
1、自由总是相对的。
就好像在一个群体里,如果没有规则,完全行为自由,就会出现各种问题。小群体还好,而对于一个大群体,就会有人被别人自由的误伤。
写软件也是一样。一个小 startup 里,API 怎么设计,代码怎么写,几个人一协商,达成共识,并不需要那么多的条条框框,也照样行的通。公司一大,代码协作的人越多,每个人的自由就会导致最终的冲突甚至问题。所以,很多大公司会制定一些 API 的 best practice,强制要求设计和实现中必须按照某种模式来做。有些规则虽有道理,但也不是说不这样不行。很多时候,就因为这样的原因,我们的 API 设计中会有很多限制,表面上似乎给设计带来无谓的难度,但是仔细考量,从规范代码一致性的角度而言,还是有很大好处的。
API 设计里很常见的一个情况是,有一个系统功能,目前并没有人使用,只是有人提出:“这种情况我们以后应该要支持。” 之前说过,由于 API 上线后再改很困难,所以在设计初期就要尽可能的考虑未来的发展。但是这些 “可能” 的应用场景因为需求的细节和使用频度都不明确,最容易造成系统的 over-design。
我记得好像听过一个说法,重新概括一下,就是:think about future, design with flexibility, but only implement for production。中文大概就是说:要考虑未来的场景,设计时留有余地,但永远只实现 production 确实要用的功能。
设计和实现里常常会有一些封装和抽象的概念。某些特殊情况下,封装再分拆的过程可能一定程度上影响 API 的速度,或者是代码质量的优化和性能的优化上有冲突。这个很难一概而论,还是要看具体代码是不是在 critical path 或者是不是一段需要很多人协作的代码。最终的选择还是要看情况。
AOP 编程本身就是一个极具争议的话题。概括说来,AOP 的理念是从主关注点中分离出横切关注点,是面向侧面的程序设计的核心概念。分离关注点使得解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用,业务逻辑同特定领域问题的关系通过侧面来封装、维护,这样原本分散在在整个应用程序中的变动就可以很好的管理起来。
因为 API 的设计和实现中有很多通用的关注点,如 Logging、Authentication、Parsing、Monitoring 等等,所以 API 成了 AOP 一个很自然的应用领域。使用 AOP 的 API design 继承了 AOP 的优势,如:代码的重用性,规整性,以及程序员可以集中关注于系统的核心业务逻辑等。但也自然而然继承了 AOP 固有的问题,如 代码的 profiling 和 debugging 等;程序员 experience 的要求以及相互协作的要求(例如改变某一个功能可能会影响到其它的功能)等。
这篇考虑到不同系统和语言情况都不太一样,因此没有涉及到太多细节。只把一些做 API 中得到的感悟泛泛写了写,大家有什么体会,留言里说说吧。
题图:from Pinterest
雷锋网注:转载请联系作者授权,并务必保留出处和作者,不得删减内容。