什么?网络接口还需要设计?不就是前端和后端的开发人员碰一碰,看需要什么,就给返回什么就好了吗?如果需求变化,不满足了,再对接口做个修改不就好了吗?这还需要设计?甚至在我提到“设计”这个词的时候,某些人就笑了,技术开发还需要设计?设计不是那些做 UI 的设计师才做的事情吗?
今天我来说说下我心目中的网络接口设计是怎样的过程?
0x00 先从接口说起
接口,interface。这里不讨论它具体的定义,我给出自己对于接口比较泛的理解。
首先,与接口息息相关的是一个边界的概念。系统与系统之间,模块与模块之间,客户端与服务端之间,都是有边界的。在各个系统、模块、端内部可以而且应该是自成体系的,这个自成的体系与其他体系应该有明显的区隔,就是“边界”。
其次,接口是在这个边界上体系之间打交道建立约定。这些内部自成的体系,通过接口与对方相互的协作,形成更大的系统。每个自成的体系,根据自身定义的功能、特性等等,与其他体系如何协作基本是固定的。一个体系只需把协作的约定暴露出来,其他体系按照约定来操作,就可以实现自己的目的,而不用在乎协作对象内部是怎么做的。这就是一个接口的作用。
再次,接口要完成体系与体系之间的协作/通信,是需要媒介的。如模块与模块之间可以是简单的函数调用,也可能是 RPC;网络上的各客户端与服务端则通过网络协议等等。
每个自成体系是可以有粒度上的大小之分的,但体系一定是有自己的边界的。比如面向对象中讲的类,也是一个体系,类对外暴露的公共方法可以认为是接口,类具有的功能能够与其他类很明显区分出其边界。多个类相互协作可以形成一个模块,模块有属于自己的边界;多个模块相互协作又可以形成更大的模块或者子系统,子系统之间有边界;系统之间又可以协作形成更大的系统,系统依然有边界。当体系与体系之间的边界不清晰时,组成的更大的体系就会是一团乱麻,维护就很困难。我们一定要定义好体系的边界及体系之间协作的接口约定,让每个体系各司其职,又能相互协作。
0x01 网络接口设计
网络接口,这里具体指产品开发上服务端与各客户端(Web,App,小程序等)之间通过 HTTP 协议进行通信,为了实现产品功能而做的约定。网络接口设计首先要确定服务端与各客户端之间协作的具体通信的数据格式。
为什么要事先确定好具体通信的格式?
产品开发过程中,研发人员被分成不同职能的小组进行协作。服务端有专门的人员,客户端有专门的人员。为了提高产品开发的效率,我们不会像流水线一样,让服务端的人开发完接口以后才让客户端的人员开始。几个小组的开发人员几乎是并行的开发,然后再联调、集成测试。有了接口的定义,确定了具体通信的格式,那么各小组之间就可以按照约定去并行的实现各自的部分,这样就有效利用了时间。
0x02 怎么做网络接口设计?
网络接口的设计,大部分情况下是从前端功能需求入手的。通过接收到的需求或者产品原型,开发人员能够比较容易的知道:
- 需要提供哪些功能的接口
- 这些接口大致如何发起请求
- 应该返回什么样的数据
其实这 3 点在一些人看来几乎是网络接口设计的全部内容了。按照这些实现,产品的功能基本可以实现了。开发人员口头沟通,直接编码,相互联调,直到接口工作正常——这正是许多创业小团队的开发人员在做的事情。
这是远远不够的!
1. 初步分析接口需求
这正是上面 3 点做的事情。
将前端的需求做好分析,基本能够知道需要哪些功能的接口才能够满足需求。大多数接口如何发起请求,需要返回什么样的数据,也可以一目了然。因为从功能上讲,服务端的大部分工作在资源的增删改查上。
嗯,我这里再做一个乐观的假设,初步分析接口以后,开发人员可以遵循 Restful 的原则,在脑海中对于如何发起请求有了概念了。
2. 综合分析接口请求/返回的数据
完成第一步工作以后,我们会发现一些接口是围绕差不多相同的实体的。比如博客系统,会有获取博客列表,博客详情,发表博客,编辑博客,删除博客,获取自己的博客等等,都围绕博客这个实体。
虽然都是实体,由于前端的展示不一样,所需要实体的属性也不一样。比如博客列表只需要博客标题;博客详情里则需要博客的内容;而我收藏的博客列表可能需要一个收藏标记。
那我们是不是直接就根据前端的需要,只返回需要的内容呢?这些内容又该怎么返回呢?
推荐的做法是:可以根据前端的需要返回,但是返回的应该是结构化的数据,而且不同类别的结构化数据数量应该尽量少。
2.1 结构化的数据是什么?
比如上面提到的博客,它是个实体,是有结构的。标题、内容、标签这些都是简单的属性,它们组合在一起形成的结构就可以表示博客这个实体。在编程语言里大概是类;在框架设计里可能就是模型。
2.2 怎么做这个结构化?
还是拿博客举例子。
现实中博客实体的结构比这复杂的多。上面代码展示了 3 种可能的博客实体返回的内容,实际情况也比这多。很显然,这博客实体有很多共用的部分,又有很多根据特定情况不一样的部分。
如果我们任由这几种结构独立的存在,服务端编码的时候,根据需要硬编码返回;客户端同样只针对接口返回的特殊字段做特殊处理。突然有一天,博客的结构中要多返回一个创建时间,所有相关的部分可能都需要修改了。
我们应该综合这些结构,提取出共性的部分,形成基础的结构,再评估特殊的部分,是整合到这个基础的结构中,还是提炼成新的结构。
这是一个合理的提炼结构的结果。
博客详情在现实中是一个很复杂的结构,而且可能包含非常大的数据量(成千上万字的内容),可以抽成独立的结构,作为博客中的 detail 的内容,根据是列表还是详情按需返回。
收藏标记,是在我的收藏列表中需要的特殊字段,它很小,直接合并到博客实体中带回也问题不大。
这样系统中需要的博客实体就简化成这两个结构化的模型了,所有地方都能用。
大家可以想一下,为什么把博客详情中的字段都挪到博客结构中,这样可以只剩下一个模型,却不是一个好的实践?
2.3 结构化有什么好处?
写 JavaScript/PHP 的同学可能没有体会到将一个结构转换成类的痛点,在 Java/OC 这样的强类型语言里面定义的差不多同一实体的类型太多,就要写很多相互转换的代码。做结构化,让结构化的类别尽量少,这在一定程度上减少了这种重复编码。这也只是一个非常粗浅的好处。
有了结构化,其他的好处包括:
- 实体的结构清晰了,将很多原来零散的属性组成一组,便于记忆和传达了。团队沟通效率提升了。
- 对实体处理的代码可以收紧到一个地方统一维护,不用再像原来分散的每个地方都需要维护了。代码维护效率提升了。
结构化的数据是参与系统运行的基石,提炼出这些结构化的数据(实体、模型、类某种程度上来讲是一样的)才能够更好的理解系统。
3. 揣摩部分接口实现的细节
网络接口设计,不是一个简单而低级的步骤。需要非常的熟悉需求乃至背后的实现细节,才可能定义出好的接口。
不同的接口有许多从细节出发的考量,这个环节我就举一些例子,大家自己体会。
3.1 获取我的博客接口该怎么发起请求?
至少有两种的可行方案:
- 请求上直接传递我的用户 ID,服务端通过用户 ID 查询出我的博客并返回。
- 请求上不传递我的用户 ID,而是在请求的 header 信息里带上 token。之后服务端通过 token 解析出用户 ID,再查询出我的博客并返回。
显然,这两种方案要求服务端上的实现也不一样。当你构思网络接口设计的时候,服务端的实现已经在你脑海中有了方案。
3.2 任务的打卡记录接口该怎么返回?
一个任务持续 10 天,每天一次打卡,可能存在某天漏打的情况。接口数据怎么返回呢?
这里我也举两种方案:
1. 返回一个任务打卡情况的数组,如:
代表任务 1,第 1,3 天打卡了,第 2 天漏打了。
2. 返回一个字符串,如:1,2,4,5
,代表第 1,2,4,5 天打卡了,第 3 天漏打了,6-10 天还没有打。
这两种接口返回数据的设计服务端的实现细节也不一样。第 1 种方案可能需要一张独立的任务打卡记录表来支撑其实现,而第 2 种方案可能只需要在用户任务表中加一个字段就可以了。
不同的接口返回,对于客户端上的处理也是不一样的。第 1 种方案,客户端解析对象列表;第 2 种方案,客户端解析字符串。
这些方案如何抉择,每个设计者都有自己的考量。当你构思网络接口设计的时候,服务端的实现已经在你脑海中有了方案,客户端上怎么处理在你脑海中也有了方案。
3.3 收藏和取消收藏接口要怎么设计?
同样两种方案:
- 收藏/取消收藏各自独立一个接口
- 收藏/取消收藏统一为一个接口,通过类型来区分具体操作
如果是针对同一实体的简单操作接口,如针对博客的收藏/取消收藏,点赞/取消点赞,每个都独立一个接口,那会导致接口数量的猛增。把它们看做对实体的操作,统一为一个接口,就可以避免这种问题。当你构思网络接口设计的时候,服务端的处理已经在你脑海中有了方案,客户端上如何调用也已经在你脑海中有了方案。
3.4 敏感的用户数据在接口中怎么保护?
用户密码,订单金额,会员积分等等这些敏感数据,你的接口设计时怎么保证安全?你想到的方案:
- 需要通过 https 协议传输。
- 需要对接口做验签,验签应该怎么做?
当你构思网络接口设计的时候,你也已经同时在考虑这些了。
3.5 这样的接口设计能不能支持未来的扩展?
之前写过一篇,你的用户登录接口是仅满足目前单一平台的登录,还是支持未来多平台登录?你该如何设计?当你构思网络接口设计的时候,这些对未来的适应应该已经在你的脑海里了。
在仔细揣摩这些接口实现细节时,同样可以归纳出很多的共性出来,如用户的登录/授权策略,接口参数做验签保护,接口响应可以有统一的结构返回、有统一的错误处理等等。这些统一的设计约定一样需要记下来。
4. 设计方案文档化
你花了很多的时间分析、思考、调研,是时候把这些重要的成果写成文档了。
写成文档有几大好处:
- 将你的思考成果可视化,便于你自己后续修改、调整,便于别人反复阅读理解你的思路。
- 作为团队各部分协作的约定,有章可循,提高协作效率。
一份具有良好可读性的网络接口设计文档应该包含如下结构:
- 标题
- 修订记录
- 通用说明
- 模型定义
- 接口定义
标题和修订记录就不说了。
4.1 通用说明
通用说明可以包含如下部分:
- 通用的接口响应结构说明。
- 需要保护的接口的验签方案说明。
- 接口通用的请求参数及传输方案说明。
- 接口的公共错误处理说明。
- 其他与接口相关的共同约定说明等。
4.2 模型定义
我们在做网络结构设计的时候,提炼出了结构化的数据,可以称之为模型。将系统涉及到的模型罗列在一起,能够为理解系统提供便利。
模型的定义需要说明模型代表的实体,用来表示实体的模型名称以及模型需要的属性名称和类型,并为属性做说明。
这里还拿博客举例子:
4.3 接口定义
接口定义应该包括:
- 接口功能描述
- 接口具体定义,如请求方法 + 接口标识
- 请求的参数说明
- 接口的响应说明
实例:
5. 总结
现在来对怎么做接口设计做个总结:
- 初步分析接口需求,遵循接口的规范来大致确定满足需求的接口。
- 综合分析接口请求/返回的数据,提炼出参与系统运作的模型。
- 揣摩部分接口实现的细节,满足符合现有系统、扩展性、安全性等需求。
- 设计方案文档化。协作规范可视化,有依据,可追溯。
0x03 最后
网络接口设计不是一个可有可无的过程。一次好的网络接口设计的过程,需要设计人员对整个需求有清晰的认识,甚至会考虑到实现细节上。团队成员一起参与网络接口设计文档的评审,是一次对需求的再次统一,是对实现细节上的一次统一。完成网络接口设计文档,在整个研发过程,是一个重要的里程碑。
有的朋友说做这个耗时,不值得。嗯,那你可能就要忍受快速开始开发之后反反复复的返工了。
- EOF -