API设计的几条原则

2020 年 8 月 24 日

API设计的几条原则

API 本身的含义指应用程序接口,包括所依赖的库、平台、操作系统提供的能力都可以叫做 API。我们在讨论微服务场景下的 API 设计都是指 WEB API,一般的实现有 RESTful、RPC 等。API 代表了一个微服务实例对外提供的能力,因此 API 的传输格式(XML、JSON)对我们在设计 API 时的影响并不大。



程序接口


API 设计是微服务设计中非常重要的环节,代表服务之间交互的方式,会影响服务之间的集成。 通常来说,一个好的 API 设计需要满足两个主要的目的:


  • 平台独立性。 任何客户端都能消费 API,而不需要关注系统内部实现。API 应该使用标准的协议和消息格式对外部提供服务。传输协议和传输格式不应该侵入到业务逻辑中,也就是系统应该具备随时支持不同传输协议和消息格式的能力。

  • 系统可靠性。 在 API 已经被发布和非 API 版本改变的情况下,API 应该对契约负责,不应该导致数据格式发生破坏性的修改。在 API 需要重大更新时,使用版本升级的方式修改,并对旧版本预留下线时间窗口。


实践中发现,API 设计是一件很难的事情,同时也很难衡量设计是否优秀。根据系统设计和消费者的角度,给出了一些简单的设计原则。


使用成熟度合适的 RESTful API


RESTful 风格的 API 具有一些天然的优势,例如通过 HTTP 协议降低了客户端的耦合,具有极好的开放性。因此越来越多的开发者使用 RESTful 这种风格设计 API,但是 RESTful 只能算是一个设计思想或理念,不是一个 API 规范,没有一些具体的约束条件。


因此在设计 RESTful 风格的 API 时候,需要参考 RESTful 成熟度模型。



RESTful 成熟度模型。


根据自己的应用场景选择对应的成熟度模型,一般来说系统成熟度模型在 Level 2 左右。


避免简单封装


API 应该服务业务能力的封装,避免简单封装让 API 彻底变成了数据库操作接口。例如标记订单状态为已支付,应该提供形如POST /orders/1/pay这样的 API。而非PATCH /orders/1,然后通过具体的字段更新订单。


因为订单支付是有具体的业务逻辑,可能涉及到大量复杂的操作,使用简单的更新操作将业务逻辑泄漏到系统之外。同时系统外也需要知道订单状态 这个内部使用的字段。


更重要的是,破坏了业务逻辑的封装,同时也会影响其他非功能需求。例如,权限控制、日志记录、通知等。


关注点分离


好的接口应该做到不多东西,不少东西。 怎么理解呢?在用户修改密码和修改个人资料的场景中,这两个操作看起来很类似,然后设计 API 的时候使用了一个通用的/users/1/udpateURI。


然后定义了一个对象,这个对象可能直接使用了User这个类:


{  "username": "用户名",  "password": "密码"}
复制代码


这个对象在修改用户名的时候, password是不必要的,但是在修改密码的操作中,一个password字段却不够用了,可能还需要confirmPassword


于是这个接口变成:


{  "username": "用户名",  "password":"密码",  "confirmPassword":"重复密码"}
复制代码


这种类的复用会给后续维护的开发者带来困惑,同时对消费者也非常不友好。合理的设计应该是两个分离的 API:


// POST /users/{userId}/password
{ "password":"密码", "confirmPassword":"重复密码"}// PATCH /users/{userId}
{ "username":"用户名", "xxxx":"其他可更新的字段"}
复制代码


对应的实现,在 Java 中需要定义两个 DTO,分别处理不同的接口。这也体现了面向对象思想中的关注点分离。


完全穷尽,彼此独立


API 之间尽量遵守完全穷尽,彼此独立 (MECE) 原则,不应该提供相互叠加的 API。例如订单和订单项这两个资源,如果提供了形如 PUT /orders/1/order-items/1 这样的接口去修改订单项,接口 PUT /orders/1 就不应该具备处理某一个 order-item 的能力。


这样的好处是不会存在重复的 API,造成维护和理解上的复杂性。如何做到完全穷尽和彼此独立呢?


简单的方法是使用一个表格设计 API,标出每个 URI 具备的能力。



API 设计表格


资源 URL 设计来源于 DDD 领域建模就非常简单了,聚合根作为根 URL,实体作为二级 URI 设计。聚合根之间应该彻底没有任何联系,实体和聚合根之间的责任应该明确。


产生这类问题的根源还是缺乏合理的抽象。如果存在 API 中可以通过用户组操作用户,通过用户的 URI 操作用户属于的用户组,这其中的问题是缺少了成员这一概念。用户组下面的本质上并不是用户,而是用户和用户组的关系,即成员。


版本化


一个对外开放的服务,极大的概率会发生变化。业务变化可能修改 API 参数或响应数据结构,以及资源之间的关系。一般来说,字段的增加不会影响旧的客户端运行。但是当存在一些破坏性修改时,就需要使用新的版本将数据导向到新的资源地址。


版本信息的传输,可以通过下面几种方式


  • URI 前缀

  • Header

  • Query


比较推荐的做法是使用 URI 前缀,例如/v1/users/表达获取 v1 版本下的用户列表。


常见的反模式是通过增加 URI 后缀来实现的,例如/users/1/updateV2。这样做的缺陷是版本信息侵入到业务逻辑中,对路由的统一管理带来不便。


使用 Header 和 Query 发送版本信息则较为相似,不同之处在于,使用 URI 前缀在 MVC 框架中实现相对简单,只需要定义好路由即可。使用 Header 和 Query 还需要编写额外的拦截器。


合理命名


设计 API 时候的命名涉及多个地方:URI、请求参数、响应数据等。通常来说最主要,也是最难的一个是全局命名统一。


其次,命名需要注意这些:


  • 尽可能和领域名词保持一致,例如聚合根、实体、事件等

  • RESTful 设计的 URI 中使用名词复数

  • 尽可能不要过度简写,例如将 user 简写成`usr

  • 尽可能使用不需要编码的字符


用领域名词来对 API 设计命名不是一件特别难的事情。识别出的领域名词可以直接作为 URI 来使用。如果存在多个单词的连接可以使用中横线,例如/orders/1/order-items


安全


安全是任何一项软件设计都必须要考虑的事情,对于 API 设计来说,暴露给内部系统的 API 和开放给外部系统的 API 略有不同。



安全


内部系统,更多的是考虑是否足够健壮。对接收的数据有足够的验证,并给出错误信息,而不是什么信息都接收,然后内部业务逻辑应该边界值的影响变得莫名其妙。


而对于外部系统的 API 则有更多的挑战。


  • 错误的调用方式

  • 接口滥用

  • 浏览器消费 API 时因安全漏洞导致的非法访问


所以设计 API 时应该考虑响应的应对措施。针对错误的调用方式,API 不应该进入业务处理流程,及时给出错误信息;对于接口滥用的情况,需要做一些限速的方案;对于一些浏览器消费者的问题,可以在让 API 返回一些安全增强头部,例如:X-XSS-Protection、Content-Security-Policy 等。


API 设计评审清单


  • URI 命名是否通过聚合根和实体统一

  • URI 命名是否采用名词复数和连接线

  • URI 命名是否都是单词小写

  • URI 是否暴露了不必要的信息,例如/cgi-bin

  • URI 规则是否统一

  • 资源提供的能力是否彼此独立

  • URI 是否存在需要编码的字符

  • 请求和返回的参数是否不多不少

  • 资源的 ID 参数是否通过 PATH 参数传递

  • 认证和授权信息是否暴露到 query 参数中

  • 参数是否使用奇怪的缩写

  • 参数和响应数据中的字段命名统一

  • 是否存在无意义的对象包装 例如{"data":{}'}

  • 出错时是否破坏约定的数据结构

  • 是否使用合适的状态码

  • 是否使用合适的媒体类型

  • 响应数据的单复是否和数据内容一致

  • 响应头中是否有缓存信息

  • 是否进行了版本管理

  • 版本信息是否作为 URI 的前缀存在

  • 是否提供 API 服务期限

  • 是否提供了 API 返回所有 API 的索引

  • 是否进行了认证和授权

  • 是否采用 HTTPS

  • 是否检查了非法参数

  • 是否增加安全性的头部

  • 是否有限流策略

  • 是否支持 CORS

  • 响应中的时间格式是否采用ISO 8601标准

  • 是否存在越权访问


作者介绍


少个分号(林宁),ThoughtWorks 咨询师,长期从事国内外企业软件开发,在 React、RESTful API 等新兴技术有大量经验,喜欢开发基本组件和工具来提升效率。目前专注于富前端和 API 开发,热衷于研究前后端的集成和解耦。"


本文转载自 ThoughtWorks 洞见。


原文链接


API设计的几条原则


2020 年 8 月 24 日 14:071418

评论

发布
暂无评论
发现更多内容

你确定你会算数吗?老大说:你连这个都不知道还敢面试电商公司?

小Q

Java 学习 编程 程序员 面试

升级redhat6的yum源替换为centos源

Bruce Xiong

架构师训练营第 1 期第 13 周作业

好吃不贵

极客大学架构师训练营

看完这份文档我吊打了BATJ面试官,他问的我全都会:Spring+逻辑算法+MySQL+Java+Redis+并发编程+JVM+RabbitMQ等

Java架构之路

Java 程序员 架构 面试 编程语言

9年技术面试官讲解:计算机专业应届生怎样写简历

Java架构师迁哥

synchronized 是王的后宫总管,线程是王妃

Java架构师迁哥

A Guide for Accidental Project Managers

Geek_ed0696

pmp project manager role of PMP PMP Certification greycampus

5年Java开发经验,7面阿里历经千辛万苦成功斩获P7及Offer!

Java成神之路

Java 程序员 架构 面试 编程语言

万万没想到我也能挤进阿里定级P7,美团架构师总结整理的这份GitHub标星150K+的Java神仙笔记是我成功的关键。

Java成神之路

Java 程序员 架构 面试 编程语言

一次资源泄露问题排查纪录

AI乔治

Java 架构 JVM 内存泄漏

太狠了阿里技术专家撰写的电子版JVM&G1 GC实战,颠覆了传统认知

Java架构之路

Java 程序员 架构 面试 编程语言

官方活动 | 日更挑战(初阶)——七日更,挑战百元京东E卡!

InfoQ写作平台

活动专区 七日更 日更挑战

图解Janusgraph系列-并发安全:锁机制(本地锁+分布式锁)分析

洋仔聊编程

janusgraph 图数据库

字节跳动总监亲自整理,在知乎高达5716赞的Java开发手记。

Java架构之路

Java 程序员 架构 面试 编程语言

Techo | 大数据专场报名盛启!12月20日欢迎莅临!

小小的一朵云

大数据 数据仓库 大数据架构

云智一体:攀登2021智能经济新山峰

脑极体

红着眼连续肝了一个月淦出的20w字的《Java核心技术总结》和3w字的《面试题总结》PDF文档,希望能够帮到你!

Java成神之路

Java 程序员 架构 面试 编程语言

朋友不讲武德急催我给他Java干货教程,我劝他耗子尾汁并丢给他一份GitHub上标星115k+的Java教程,他看了之后连忙向我道歉!

Java架构之路

Java 程序员 架构 面试 编程语言

Java 并发编程:volatile能否保证数据的同步

码农架构

Java Java并发

又真香了!到底是怎样的JAVA面试文档,拿到这么多offer

Crud的程序员

程序员 架构 java面试

免费分享!GitHub标星15k的Java编程思想最新中文版,肝了一周整理成1539页的PDF文档!

Java架构之路

Java 程序员 架构 面试 编程语言

【变与不变】架构中的边界划定

soolaugust

编程 架构 设计

程序员什么时候就该辞职了?

Java架构师迁哥

我把2020年GitHub上最火最牛b的Java进阶教程和实战项目等整理成了一个PDF文档,免费分享给大家。

Java成神之路

Java 程序员 架构 面试 编程语言

2020年第11期公有云性能评测:盛大云-华东实现“三冠”,百度云虎视眈眈

BonreeAPM

百度云 腾讯云 阿里云 公有云 华为云

ROS 机器人操作系统进阶实战

Geek_3cc3ec

Python 人工智能 学习 ROS

芯片破壁者(二十二):政府与半导体间的“美国往事”

脑极体

图解Janusgraph系列-图数据底层序列化源码分析(Data Serialize)

洋仔聊编程

janusgraph 图数据库

冒着被劝退的风险免费分享给大家一份阿里内部绝密《百亿级并发系统设计》实战教程。

Java成神之路

Java 程序员 架构 面试 编程语言

LeetCode题解:127. 单词接龙,BFS+生成所有可能新单词再匹配,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

校园恋爱新技能:智慧琴房恋爱助攻手册

IoT云工坊

物联网 API sdk 智慧琴房 智慧校园

API设计的几条原则-InfoQ