API 设计的原则

在这里插入图片描述

我在 Thoughtworks 上曾经读到过一篇文章《API 设计的几条原则》我觉得写得非常不错,链接我放底下,推荐阅读。

在我多年的工作历程中,我深刻意识到 API 的设计至关重要,但是我发现,在一般的组织中,API 的设计却往往草率。这主要体现在几个方面:

  1. 操刀人选草率 :不少团队的 API 设计,都是负责实现的同事进行的,这些同事很多情况下,并不是团队里最资深,最了解各个系统构造的人,在设计的时候,就不能考虑到更多的东西;
  2. 接口缺乏标准 :公司稍微大一点的情况下,很容易缺乏全公司统一的标准,往往就是各个团队有各个团队的标准,如果仅是如此尚好,有时候不同团队的标准不兼容;
  3. 命名草率:接口上选用的单词,往往非常随机,取决于设计者的个人修养,而这些命名有时候并某有经过审计,在定稿的时候,上下游都没有认真的思考过,有时候名字取得过大,有时候取得过小,非常妨碍使用者的理解,甚至造成误导;

我观国内外一些顶级的开源项目,API 动辄几年十几年不变,就深感佩服。那么 API 设计到底要考虑些什么因素,怎样设计,才能做到这样的稳定呢?

API 本质上是系统不同部分之间通信的协议,当然也包括跟外部系统之间的通信。要想设计得好,就要将系统充分的抽象,这个过程之中,要尽量摒弃掉抽象之外的因素的影响。比如,接口运行的网络协议(TCP/UDP),内容格式(文本/二进制),调用者的形态(服务器/客户端),这类都不应该预定一种假设。

原文的作者认为,设计 API 是非常难的,评估 API 也是非常难的,所以给出一些原则,共参考:

使用成熟度合适的 RESTful API

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

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

成熟度等级解释示例
Level 0定义一个 URI 所有请求通过 POST 完成POST /?action=changeUserPassword
Level 1创建独立的资源地址,隔离 API 的范围POST /user?action=update
Level 2使用 HTTP 动词定义对资源的操作GET /user/01
Level 3使用 API 超媒体 (HATEOAS,返回的 body 中索引相关的资源地址{ "links" : [ "/users/", "/products/" ] }

一般团队选用的等级在 Level 2,这是一个参考,不过我自己实践下来看,Level 2 也是比较难的,我所在的团队可能还在使用 Level 1,因为更直观一点,比较容易掌握,大家的理解也比较容易一致。

避免简单封装

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 具备的能力。

资源 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标准
  • 是否存在越权访问

《API设计的几条原则》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/734293.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

K8S - 在任意node里执行kubectl 命令

当我们初步安装玩k8s (master 带 2 nodes) 时 正常来讲kubectl 只能在master node 里运行 当我们尝试在某个 node 节点来执行时, 通常会遇到下面错误 看起来像是访问某个服务器的8080 端口失败了。 原因 原因很简单 , 因为k8s的各个组建&…

外包干了6天后悔了,技术明显进步。。。。。

先说一下自己的情况,本科生,19年通过校招进入广州某软件公司,干了接近3年的功能测试,今年年初,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试…

chrome高内存占用问题

chrome号称内存杀手不是盖的,不设设置的话,经常被它内存耗尽死机是常事。以下自用方法 1 自带的memory saver chrome://settings/performance PerformanceMemory Saver When on, Chromium frees up memory from inactive tabs. This gives active tab…

基于Springboot的在线租房和招聘平台(有报告)。Javaee项目,springboot项目。

演示视频: 基于Springboot的在线租房和招聘平台(有报告)。Javaee项目,springboot项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结…

python基础——条件判断和循环【if,while,for,range】

📝前言: 这篇文章主要讲解一下条件判断语句if和循环语句while,for在python中需要注意的地方。 建议已有一定了解(对语句的执行逻辑清楚)的读者观看,如果对条件判断和循环的执行逻辑不太清楚,也可…

bug_java

文章目录 1.创建Maven时: idea报错为:java:错误:不支持发行版本52. Springbot启动报错-类文件具有错误的版本 61.0, 应为 52.0 1.创建Maven时: idea报错为:java:错误:不支持发行版本…

遗传算法GA求解机器人栅格地图最短路径规划,可以自定义地图及起始点(提供MATLAB代码)

一、原理介绍 遗传算法是一种基于生物进化原理的优化算法,常用于求解复杂问题。在机器人栅格地图最短路径规划中,遗传算法可以用来寻找最优路径。 遗传算法的求解过程包括以下几个步骤: 1. 初始化种群:随机生成一组初始解&…

小程序网页view多行文本超出隐藏或显示省略号

实现效果: 限制两行,超出即显示省略号 实现:话不多说,展示代码 关键代码 .box{ width:100rpx; overflow:hidden; text-overflow: ellipsis;//超出省略号 display:-webkit-box; -webkit-line-clamp: 2;//显…

RabbitMQ - 03 - Work消息模型

目录 部署demo项目 什么是Work消息模型 实现Work消息模型 1.创建队列 2.生产者代码 3.消费者代码 4.配置yml 部署demo项目 通过消息队列demo项目进行练习 相关配置看此贴 http://t.csdnimg.cn/hPk2T 注意 生产者消费者的yml文件也要配置好 什么是Work消息模型 工作…

Arcgis小技巧【20】——属性表有东西,缩放至图层却看不到?

一、问题分析 这是一个看似正常的要素类数据,打开它的属性表,里面有一行要素: 说明这不是空数据。 回到地图界面,在图层列表中,右键单击,点击【缩放至图层】。 使用过这个功能的应该知道,点击…

快速上手:使用Hexo搭建并自定义个人博客

🌟 前言 欢迎来到我的技术小宇宙!🌌 这里不仅是我记录技术点滴的后花园,也是我分享学习心得和项目经验的乐园。📚 无论你是技术小白还是资深大牛,这里总有一些内容能触动你的好奇心。🔍 &#x…

Linux常用操作命令-防火墙常用操作

一、防火墙常用操作 1、查看防火墙的状态; systemctl status firewalld 2、启动防火墙 systemctl start firewalld.service 3、开启某个端口,如8081端口,输入命令 firewall-cmd --zonepublic --add-port8088/tcp --permanent 4、删除某个…

Linux第74步_“设备树”下的LED驱动

使用新字符设备驱动的一般模板,以及设备树,驱动LED。 1、添加“stm32mp1_led”节点 打开虚拟机上“VSCode”,点击“文件”,点击“打开文件夹”,点击“zgq”,点击“linux”,点击“atk-mp1”&am…

AHU 数据库 实验四

【实验名称】 实验4 数据库的嵌套查询和集合查询 【实验目的】 1. 理解并掌握子查询的概念和作用; 2. 掌握DBMS 实现嵌套查询的基本方法和应用; 3. 掌握DBMS 实现集合查询的基本方法和应用; 4. 学习、掌握并熟练…

selenium-java 通过配置xml文件并发运行类或者方法

1、打开idea允许某个class类,可以在控制台看到运行路径的下的配置文件如下图: 2、将路径复制到本地路径中找到temp-testng-customsuite.xml文件 3、复制该文件到项目的根目录下,可以修改文件名称,如下图 4、如图所示,通…

Unity UGUI之InputField(TMP)基本了解

Unity的InputField组件是用于在Unity中创建可供用户输入文本的输入框的UI组件。通过InputField组件,可以让用户在运行时输入文本,比如用户名、密码、搜索关键字等。其中TMP版本的InputField是基于TextMeshPro的InputField组件,提供了更多的文…

C++字符串操作【超详细】

零.前言 本文将重点围绕C的字符串来展开描述。 其中,对于C/C中字符串的一些区别也做出了回答,并对于C的(string库)进行了讲解,最后我们给出字符串的不同表达形式。 开发环境: VS2022 一.字符串常量跟字…

运维随录实战(12)之node版本管理工具nvm

1,下载安装nvm 可以去其 github 主页下在,地址为 github.com/coreybutler…会看到有很多个文件可供选择: 这里稍做下解释: nvm-noinstall.zip: 这个是绿色版本,不需要安装,但是使用之前需要配置环境变量;nvm-setup.zip:推荐下载这个包,无需配置就可以使用;Source …

Git版本工具学习

目录 版本控制git配置工作区域文件状态git对象模型基础命令.gitignore忽略文件IDEA集成Git 版本控制 本地版本控制:在本地记录每一次版本更新。 集中版本控制:版本数据都保存在单一服务器,不联网就看不到版本信息。SVN 分布式版本控制&…

虚函数与纯虚函数有什么区别?

总的来说有两点区别: 1.虚函数的作用主要是矫正指针(口语化的说法) 2.虚函数不一定要重新定义,纯虚函数一定要定义(口语化的说法) 1). 虚函数的作用主要是矫正指针,使得基类的指针…