摘要:接口其实是一种规范,在生活中随处可见,比如:不同厂商的水管使用统一的水管接口对接、电脑厂商和配件厂商按照统一的 USB 接口标准进行生产完成配对、应用程序之间通过 API 进行信息交互。
接口其实是一种规范,在生活中随处可见,比如:不同厂商的水管使用统一的水管接口对接、电脑厂商和配件厂商按照统一的 USB 接口标准进行生产完成配对、应用程序之间通过 API 进行信息交互。
亚马逊集团创始人贝佐斯规定,亚马逊的所有系统、团队之间的交互,都必须通过服务接口,现在云计算技术非常火爆,其起源实际上是亚马逊的这种 API 文化,可以说,API 构建了当前数字化世界的通路。
翻译过来就是:
所有的团队必须通过服务来对外暴露数据和功能。所有的团队之间的功能,必须通过这些服务接口来进行交互。网络上不允许任何除服务接口的其它交互方式。(不能通过 Excel 等文件传输)不管具体使用任何的技术,都必须符合上面的3条原则所有的服务接口必须都被外部化。任何不遵循这个指令的人将会被解雇。所以,软件世界中到处存在的 API,到底应该如何实现?有哪些设计技巧?接下来让我们一起探索吧。
02接口设计契约式设计原则
Design by Contract (DbC)是一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式,可以说它是 API 设计的指导书,一个好的 API 实现之前,先要回答三个关键问题:
API 期望什么?API 要保证什么?API 要保持不变的什么?首先,API 必须保证输入是接收者期望的输入条件。比如:API 需要 A、B、C 三个参数,那么使用者要提前准备好这三个参数,当条件不满足时,会拒绝请求,当条件满足时,才会处理请求。
其次,API 要保证输出结果的正确性。API 在处理过程中遇到各种异常或错误,需要在内部做好处理,并最终将正确或期望的结果输出给使用者,而不应该把错误抛给其他系统去处理。
最后,API 必须要保证处理过程中的一致性。比如,同一个 API 被部署在10个服务器上,当输入条件发生改变时,所有服务器的内部的会话、状态、API 的输出也应该保持一致。
契约式设计原则作为指导思想,那么我们在设计 API 过程中,到底应该注意哪些细节?以下是我总结的六大设计原则:
2.1 关注点分离
作为面向对象 SOLID 原则中的“I”,对我们的 API 接口提供了非常有实战意义的指导价值,所以当你在设计一个 API 的时候,不要在一个接口中杂糅多种功能,更不要用很多个 Flag 联动多个入参,这样只会让你的接口变得异常混乱。举个例子,客户只需要一个 USB 接口,而你却给了他一个交换机,他怎么知道那根线插哪里?要插多少根?排列组合都有 N 种,客户直接原地崩溃。所以说,我们在设计接口时,要力争减少使用者的认知负担,提供单一纯粹的功能。
2.2 自我表达
一个好的接口,客户在拿到后能很简单的读懂,在开发联调过程中,能根据错误信息自己修正,那么这个接口就具备了良好的自我表达能力,你可以从以下两大方面去考虑:
接口命名
接口命名应该给使用者提供一种简单且直观的体验,从接口名即可了解此接口的具体功能。
使用被大家接受的通用缩写,如:Information->info,delete->del,message->msg 等。
通过命名含义描述接口,比如获取用户列表:GetUsers。注意此方式适用于 RPC 协议接口风格,RESTful 风格不使用此形式,REST 使用名词 URI+动词形式。
错误信息
具有自表达的错误信息字段,且应该包含两点内容:
A:明确的错误指示。例如:“amount金额字段格式有误”。
B:错误后的解决方案。例如:“token字段缺失,请先登录”。
错误码设计
A:屏蔽内部逻辑。不要轻易暴露内部处理逻辑规律,防止接口被攻击。
B:便于快速定位问题。
错误码定义可以参考 HTTP 状态码,采用分段式,形成“肌肉记忆”,方便定位问题,也易于扩展。
分段分段描述1XX信息,服务器收到请求,需要请求者继续执行操作2XX成功,操作被成功接收并处理3XX重定向,需要进一步操作以完成请求4XX客户端错误,请求包含语法错误或无法完成请求5XX服务器错误,服务器在处理请求过程中发生了错误2.3 互斥穷举
接口之间应该尽量遵守MECE(互斥穷举)原则,不应该提供相互叠加的接口。例如:如果 PUT /orders/1/items/1 接口用于修改订单项,那接口 PUT /orders/1 就不应该具备处理某个 order-item 的能力。
2.4 快速失败
接口要快速验证条件,不满足要返回失败,不要等到已经击穿到应用最底部后,再去报错。这是“防御式编程”的一种理念,一个具体的实践就是对于接口入参进行校验。g
2.5 幂等性
什么是幂等?简单来说,就是当一个操作多次执行所产生的影响均与一次执行的影响相同,则它是幂等的。现代服务逐渐往分布式架构发展,服务是分散的,如何保证并行调用的幂等?
以下是常见的几种方式:
使用唯一标识符
为每个请求生成一个唯一的标识符,并在服务端进行验证。如果服务端已经处理过该标识符对应的请求,则直接返回之前的结果,而不进行重复处理。
使用状态机
对于一些有明确状态的资源(如订单、支付等),可以通过状态机来确保操作的幂等性。每个状态转换只能发生一次,重复请求不会改变状态。
使用数据库唯一约束
在数据库层面使用唯一约束来防止重复数据插入。如果重复请求尝试插入相同的数据,数据库会抛出异常,服务端可以捕获并处理该异常。
使用乐观锁
我们在更新资源时检查版本号是否匹配。如果不匹配,说明资源已经被其他事务修改过,当前事务应该回滚。
使用悲观锁
悲观锁假设数据在大多数时间会发生冲突,读取数据时就加锁,防止其他事务修改数据,通常通过数据库的行级锁来实现,如使用“for update”。
使用分布式锁
分布式锁与悲观锁本质上相似,都通过串行化请求处理来实现幂等性。与悲观锁不同的是,分布式锁更轻量。在系统接收请求后,首先尝试获取分布式锁。如果成功获取锁,则执行业务逻辑;如果获取失败,则立即拒绝请求。
2.6 版本化
一个对外开放的服务,极大的概率会发生变化。业务变化可能修改 API 参数或响应数据结构,以及资源之间的关系。一般来说,字段的增加不会影响旧的客户端运行。但是当存在一些破坏性修改时,就需要使用新的版本将数据导向到新的资源地址。
总结
比较推荐的做法是使用 URI 前缀。常见的反模式是通过增加 URI 后缀来实现的,例如/users/1/updateV2。这样做的缺陷是版本信息侵入到业务逻辑中,对路由的统一管理带来不便。
使用 Header 和 Query 发送版本信息则较为相似,不同之处在于,使用 URI 前缀在 MVC 框架中实现相对简单,只需要定义好路由即可。使用 Header 和 Query 还需要编写额外的拦截器。
03接口维护3.1 文档
本人强烈反对单独使用文档维护接口,因为一旦文档和代码割裂只会逐渐缺失维护不再更新,要善于利用工具让代码和文档关联,自动更新。比如:开源的 swagger。
3.2 日志
合理区分和使用 error、warn、info、debug、trace5 种日志级别。error 较严重,对业务有影响,warn 是警告,对业务影响不大,需要开发关注。日志要打印方法、入参和出参,方便定位问题,携带关键的信息参数,如 userId,traceId 等关键信息。统一使用合理的日志格式,包含基础的信息如:当前时间戳、日志级别、线程 ID 等。遇到条件分支,如有必要,可以在分支首行打印日志,方便定位问题,理清逻辑走向。使用异步方式输出日志,可以提升接口性能。禁止在线上开启 DEBUG,可能会打满磁盘影响业务系统运行。日志文件可以考虑单招等级分离,比如 Nginx 的 access.log 和 error.log。核心功能模块建议打印完整的日志,方便快速定位问题。来源:正正杂说