一、由来和简介
现在我们团队内说开发服务端接口,一般不做特别强调,都指的是 RESTful API。那RESTful API是什么,怎么设计和开发呢?虽然网络上已经有大量相关的资料,但我们在做招聘面试时,发现有不少同学不是很理解,或是在使用时不会变通、受其束缚,所以这个话题需要继续聊聊,介绍基本概念的同时分享一下实践经验。
软件开发从单机软件到互联网化,从互联网单体应用到系统拆分、前后端分离、服务化,接着到后来移动互联网兴起,一度有“mobile first”的策略,再到现在的微服务。可以说软件架构越来越庞大复杂,相关联的各部分需要划分得更细、职责更专一,这样各部分之间的交互就很频繁。RESTful作为一种架构风格、一种接口设计规范,简单、标准、易扩展让其得到了越来越多的应用。
REST 全称 “Representational State Transfer”,乍看很晦涩的名字,翻译为“表现层状态转化”,全称应叫“资源表现层状态转化””。“资源”指服务中的实体或信息,“表现层”指信息的展现形式,如json、xml、html等,“状态转化”指接口访问过程中数据和状态的变化,作为http无状态服务,业务的实现必然要涉及到数据和状态的变化。
二、资源
在RESTful API中,“资源” 使用名词形式的URL表示,且一般为复数,如“/users”,使用具有“ID”同等作用的数据限定单体,如 “/users/666”或”/users/zhangsan”,使用URL连接表示所属或关联关系 如 ”/users/666/projects”表示ID为666用户的所有项目。
接口的版本号,一般也放在URL中,简单方便,易于监控, 如”/v1/users”。 也有将版本号放在http header中,或者有的将主版本号放URL中,小版本好放httpheader中。
三、表现层
“表现层”一般采用JSON字符串,作为接口数据传输,相对于XML,JSON虽然在表现力上若一些,但可读性、数据简洁性上都好很多,且和前端Javascript天生就是一对。接口返回最好采用统一的格式(如:{code: 200, msg:”OK”,data:{}}),方便使用者。其中“code”可以表示特殊业务相关的状态,如定义code为600时表示添加的用户名称重复,在无特殊含义时建议和http status保持一致以减少认知上的负担;”msg”给出必要的简短描述,在某些场景考虑到安全性,可以简化甚至将“msg”留空,如一个未授权的用户访问资源,接口返回http status为401, “msg”不要再做出过于详细的原因说明;“data”可以是对象或列表,若是分页的列表,采用统一的分页数据格式说明分页信息——如当前页码、总页数、总条数。
四、状态转移
“状态转移”可以使用基本的httpmethod和上面的URL形成一个动宾结构,如“POST /v1/users”表示创建一个用户,在http body中提交用户详细信息,成功后接口返回 {code:200, msg:”OK”,data:{id:666, name:”zhangsan”}} 。 一般常用的有GET、POST、PUT、DELETE*,分别表示获取、创建、更新、删除,而HEAD表示获取接口元数据。对于GET、PUT、DELETE、HEAD要实现接口的 幂等性,而POST请求根据业务需要也可以实现其幂等性,根据系统实现复杂度,POST请求实现幂等性难度会更大些,特别是在微服务场景下。
五、RESTful规范带来的一些 “困境”
采用一套规范的同时往往也意味着有限制,会对接口设计造成一些困境。
http method有限的几个动词,有时不足以表达接口含义,如“订阅一个项目”(在项目更新时给订阅者发送通知),对资源”/v1/users/666/projects/1”加任何一个httpmethod都不足以表达。此时可以采用变通的方法:一般可以在资源路径末尾加动词或将操作动作抽象为一种实体对象,抽象出来的实体对象尽量采用常规的名称,要易于理解,避免异想天开造轮子。如POST /v1/users/666/projects/1/subscribe 表示订阅用户666的ID为1的项目,POST /v1/users/666/projects/1/star 则将“收藏项目”这个动作抽象为“给项目加一颗星”。
再比如对“/projects”有复杂的查询需求,URL用任何一个“资源”路径都不好表达,且查询参数还比较多——此时就不要再固守“查询只能用GET”,可以设计为POST /v1/projects/search,查询参数太多不适合当作queryparameter 放在URL后面则可以放到http body中。
考虑到很多监控系统,不会去读取httpPOST请求的body内容,在很需要做监控的接口上,甚至可以考虑将POST请求的body部分改为在URL的query参数,如 POST /tasks?name=xxx&template=yyyy 。对于固守规范的同学会很诧异既然用了POST 怎么还将参数放到URL后面——在实际需要很大时,是可以牺牲一些设计上的“完美”的。
一般来说,每个接口职责要清晰明确,业务方根据需要去组合编排这些接口来实现业务。在某些必要的场景也可以将多个简单接口的功能组合到一个大的接口中,以减少接口访问在时间和性能上的损耗。但这样的做法不能太多,否则接口容易交叉、混乱、职责不清。
总之,不要死抱着规范不放,在必要的地方要做变通,多看看一些好的示例学习,如github的接口。
六、领域模型分析和RESTful API设计的关系
在得到一个业务系统需求后,在开发服务接口之前,我们会有一步是对其做领域模型分析,提炼出该业务系统涉及的各种实的或需的领域对象,并识别它们之间的关系。对某些业务系统来说,完成这些领域对象的CRUD操作及它们之间关系的组合变化,就能支撑大部分的业务需求。那这和RESTful API 有什么关系呢?相信到这里你不难发现,RESTful 规范中URL表示的“资源”、“资源”间的关联关系,以及“状态转移”部分说到的http method + URL的动宾组合,都能比较好的继承我们领域模型分析的结果,很好的过度到RESTful API的设计。当然,不表示领域模型里的所有“实体”都会对应到RESTful API中的URL,在接口设计中结合实际情况肯定会有相应的取舍和变化。比如领域模型中分析出来的某个实体可能只是体现在数据存储层,没到接口层,或是这个所谓“实体”其实是多个概念的综合表现,体现为多个小的实体及其关系。
通过领域模型分析,结合DB数据层技术上的考量,可以将需要落库的对象进一步分析得到DB的ER图设计。至此,至下往上从数据层、之上往下从接口层都清晰了后,中间层的代码就容易组织得清晰,而一个新来的同学也比较容易在这个框架内去理解和扩展。
七、RESTful API设计对开发的影响
在一个团队中,如果没有清晰的系统设计、接口设计,特别是新人,在拿到一个业务需求时,容易仅仅面向当前需求来编写代码,缺少一个通用的、长远的考虑。如要提供一个接口给xxx系统“创建任务”,接口名甚至可能被定义为/createXxxTask,接口内部逻辑实现上自然也会加上针对这个业务的一堆if else。可以想象在接入的业务需求多了后,会有很多同质化的接口,也会存在不少大同小异的代码重复。
转换一个角度,理解要实现的业务需求后,将目光由外转到系统内。根据以上提到的领域模型分析、RESTful API设计,我们对当前系统有哪些“资源”,他们组合关系是怎样的,进而得出系统目前能提供哪些能力,还欠缺什么,然后再在这上面去扩展。尽量将这个需求本质上要操作哪些资源和关系提炼出来,设置更加通用和可扩展的接口。
相对于天马行空无章法地接口设计,RESTful接口设计规范在给设计着开发者戴上一副脚镣的同时也让其舞得更美。
感兴趣的可以进一步了解 接口身份认证、HATEOAS**、各种对数据的序列化方式以及RPC 方案和RESTfulAPI 方案在技术选型上的取舍。
接口设计过程中可以使用一些工具、文档来沉淀设计成果,同时用于加强在设计、开发过程中团队之间的交流协作 —— 这部分内容留待下次再聊。