从App角度看API (RESTful) 设计

要API,不要做外星人 (图片来自网络)
首发:http://www.jianshu.com/p/ace1428888ca

做了10多年的桌面和逻辑模块的开发,两年前才开始接触互联网这一块,说起来对RESTful API是没有太多经验的。
公司app搭建之初,前后端通力合作,期间同不少后端同事就API的设计都有过沟通交流;到现在app上线也要快满一年了,不久前进行了一次大改版,部分API也从v1升级到了v2,觉得有些经验,可以总结一下。

一. API设计的一些基本原则

  1. API是自述的
    大多数我看的资料所写的Intuitive(直观的、直觉可理解的),也有些写Descriptive(描述性的),我将其命名为“自述”。一个API有自述性,也就是说看到API的URL,就知道这个API是要干嘛;且这个API的返回值中的字段,又能很好的解释其返回的内容。
    虽然API文档是不可或缺的,但是如果看到API的URL和API的返回值字段就知道这个API的功能作用,多好!
    (注:下文有实例)
  • API是完备的
    对于一组API,我们会要求其为最小完备集。
    对于一个API,个人觉得其同样有最小完备性。这里我主要是指在各种输入参数情况下,API的返回都应该是合理的、完全的。(实际工作中我发现为了满足各种不同的需求,有时候API的返回值中会插入一下冗余信息,因此我这里只提完备性。)
    (注:下文有实例)

  • API是抽象的
    在软件工程中,一直都有各式各样的Add a Layer of Indirection,即通过一层抽象,屏蔽掉具体的数据/实现/细节。使用领域和表现形式各异,其原理实则相同。
    API的设计同样适用。
    (注:下文有实例)

  • API是兼容及可扩展的
    一个API可能需要同时服务于不同的平台:Web、iOS、Android等,也可能需要服务同一个平台的不同版本。虽然可以通过创建不同的API版本(Versioning)达到相同的目的,但是如果同一个API就能做到,岂不更好?
    (注:下文有实例)

  • 其它
    另有很多重要特性,比如安全性Tracking等,但是我个人觉得和我想表达的主题还是有差异。如需更多了解,请参看文章最后的链接。

二. 我亲历、验证了的API设计Tip

1. 使用 JSON Object (Dictionary/HashMap)

JSON格式简美,是现在流行的通信格式。上一句是废话,其实我想说的是,相对于Array,使用Object (Obj-C/Swift中可与Dictionary互转,Java中可与HashMap互转) 能够让API有更大的腾挪空间。

比如有个搜索视频关键字,返回视频列表的API,要求能够让客户端对视频列表进行分页浏览。最开始设计返回的是JSON Array,在HTTP header中带入总页数供客户端进行分页处理。

这个API的设计简明直白:你需要列表,我返回Array。因此服役了不短的时间。
后来需求变了,要求在用户搜索特定关键字或者出现特定视频的时候,在页面上加入特殊的Label。然后,然后...不得不重新设计了v2版本的API。为了继续服务老版app,后台需要维护两套API。烦!

若最开始这个API就设计成JSON Object,则有好处如下:

  1. 总页数不必带在HTTP header中,整个API的信息都集中在Object内。即是我上面提到的“API的自述性”
  • 对于新的需求,增加一对Key-Value即可,老版app和新版app采用同一个API,不需要额外的逻辑去维护两套API。即是我上面提到的“API的兼容和扩展性”
2. API不要返回后台数据库的index(如自增长ID)

前端对后台资源进行引用时,常需要一个唯一标识,比如xxID之类。当时后台小伙极力说服我使用数据库中的自增长ID,被我否决了。
一般而言,生成一个全局唯一的UUID或者标识性String都是不错的选择。

前些时则发生了另外一件事,很能说明些问题。有个资源文件比较庞大,我采取了如下的使用和更新策略:

  1. app内用本地文件的方式预存一份数据 (version=1)
  • 当app内需要用到这个数据时,先查找缓存,再查找本地文件,这样可以保证以最快的速度获取到数据进行展示
  • 同时,app走API向后台获取这个文件的新版本(带入version=1的参数)
    • 若数据没有更新的版本,后台返回空
    • 若数据有新版本,则下载并缓存

这种方法对数据量大、更新不频繁、后台对数据容忍性大的API非常适用。
可惜上线前突然bug了。最后查找原因,发现是因为之前都在测试服务器上测试,本地文件保存的数据中含有后台数据库的自增长ID。上线前在production服务器上一跑,查无此人。

上面说了一个不使用后台数据库自增长ID的具体例子。也即是前面提到的“API的抽象性”。

3. API获取资源要精准完备

由于业务逻辑的需要,常规的API设计可能会有疏漏时,需要根据情况仔细斟酌。

比如有个网站搜集了过去一年和未来半年全世界所有的公开课、讲座和会议信息,当用户进行浏览时,默认显示当前时间点以后的50条信息;当用户往下翻至第50条时,继续加载后50条;当用户往上翻至第一条并pull整个列表时,前向加载过去的50条信息。

初看起来这个API和前面提到的视频列表很相似,但是存在如下条件:

  • 用户每次刷新时,都有可能有新的公开课、讲座或会议成为过去时,但是用户在连续刷新的过程中,应该尽量看到完整的(无缺失且不重复)的信息
  • 同一时间可能有多个公开课同时开始
  • 后台数据在不停添加,有可能在用户的某两次刷新间隔,就有了新的数据。

因此,传统的分页方式肯定是不行的。中间构思过以第一次浏览时间点作为基准的设计,也被找出了n多问题。

最终我们采用的是以UUID为基准的设计。

  • 当用户第一次浏览时,会根据用户的访问时间点,对所有条目进行时间+自增长ID的二次排序,并返回前50条
  • 当用户向下翻页时,提供最后一个条目的UUID作为参数,后台搜索到其时间和自增长ID,然后同样以时间+自增长ID作为过滤条件进行排序,并返回前50条
  • 当用户向前翻页时,提供第一个条目的UUID作为参数,同上。

举这个例子,主要是想说设计API一定要仔细谨慎,使其具有完备性。但是,让人遗憾的是,这个API其实在某些情况下还是会有遗漏,对照前面的3个条件,你能发现问题吗?

4. API默认值的意义

iOS平台编程中,UIView类提供了hidden属性,用于隐藏此窗口。为何用hidden而不用shown呢?因为窗口默认是显示的,对应着属性的默认值NO(false)。
这样的设定同样适用于API的设计。

比如app中采用统一的广告策略:使用webview加载广告页。但是广告页的展示和动画则可能有两种形式:

  1. 广告页有navigation bar (push进来或者包在navigation bar中被present出来),并显示navigation title。
  2. 广告页被全屏present出来

绝大多数情况下,广告页都采用第一种方案展示,但是特殊的广告页可能会要求采用第二种方案展示(比例较低)。
后台API提供广告URL,以及展示广告页的形式。

这个例子中,显示navigation bar的广告页就是默认情况,因为显示navigation bar的概率大。
在我明了上面这段分析之前,API是这么设计的:

{ URL: "https://xxx", nav_title: "NOT Ads" }  // 糟透了

这里,nav_title承担了双重责任:

  • 如果nav_title不为空字符串,广告页显示navigation bar并设置navigation title。
  • 否则,广告页采用全屏展示;

初看起来这样的设计好像也挺不错。但是由于nav_title的默认值(nil)并不对应广告页的默认展示形式(带navigation bar),可能无法应对新的需求变更。
比如以后因为要加入新的展示形式而需要废弃掉nav_title、替换成别的字段时,会发现nav_title删不得。WHY?因为老版本的用户必须依赖于这个参数的非默认值,简直太糟糕了。
至于如何设计这个API才算好,那就见仁见智了。

5. 全局HTTP header

全局HTTP头很金贵,因为一旦设置,则所有的API都会带上,增加数据量+消耗流量。一般来说,用户信息、平台和版本信息等都是不可缺少的,更多的可以参看我后面给出的链接。
我这里要提醒的是,现在的App设计中,总难保不打开一些web页面,最好记得在webview的HTTP header中做同样的处理哟~

6. 常量Key

上文提到API返回最好是一个JSON Object (Dictionary/HashMap),便于扩展。本节标题里说的Key,就是键值对(key-value)中的key,而常量就是我们通常意义上的const。也就是说,我们用于通信的API接口,其key最好能写成const

比如app请求后台的广告业务数据,参数是placementID,后台根据配置,获取该placementID所对应的广告展示类型adType,然后把数据adData返回给app。

一种意见是用placementID做为key,客户端根据placementID来获取广告,感觉十分直接。结构如下:

{"ads" : {placementID: adData}}

但是app端在这里需要额外的判断adData所隐含的展示类型,认为不妥

另一个种意见是以广告的展示类型adType作为key,app端根据UI的展示方式获取数据,贴近实现。结构如下:

{"ads" : {adType: adData}}

但是这种方法一是丢掉了placementID的信息,扩展性上存在问题(比如一次性请求多个placement的广告时);二是广告的展示类型可能会随业务变化,作为key时同样有兼容性的问题。

但是按照本节的标题所建议的设计方式就不存在这方面的问题了。结构如下:

{"ads" :
 [
  {"ad_placement_id": placementID,
 "ad_type": adType,
 "ad_data": adData},
  ...
 ]
}

在上面最后的实现中,所有的key都是const,即"ads", "ad_placement_id","ad_type""ad_data",整个API都很容易扩展,保持良好的兼容性。

三. 别人总结的经验

网上也有一些经验总结,可以参考:

  1. HTTP API Design Guide (有中文翻译)
  2. Best Practices for Designing a Pragmatic RESTful API
  3. API Design Principles (QT的API设计原则--Restful API和Lib API大同小异)
  4. 虚拟研讨会:如何设计好的RESTful API?(InfoQ的一个总结)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容