前言
在软件产品或项目开发过程中,往往涉及到大量API接口的开发任务。而一个接口的诞生如果是令人费解的、痛苦折磨的以及有严重后遗症的,究其根本原因还在于设计API接口的时候不够清晰、合理以及缺乏长远考虑。我依据多位同事的问答、实际工作的经验和各类辅助工具来谈一谈API接口的设计这一主题。
我将今天想要谈的范围圈定于狭义上的中小规模的业务功能前后端RESTful API
接口开发,对于超过范围的内容可能因水平不够而谈的不够准确深刻。主要内容包括我工作中经常遇到的问题和个人的检讨思考。
定义
“API接口可以使不同的软件组件之间进行无缝的通信和交互,从而提高了软件开发的效率和灵活性。 ”典型的软件组件之间就是前端和后端之间,效率和灵活性是我们要追求的目标之一。
那么我们实际开发的API达到目标了么?如果以是否实现了业务功能为目标,那我相信大部分开发人员都能拍着胸脯保证我写的API绝对能实现功能,而且极度自信绝无可能存在bug。但如果以是否高效和足够灵活来判断,则:
- 一是没有切实的标准来衡量(各类开发语言、工具标准大相径庭)
- 二是也没有权威的裁判来评判(一般都是前后端相互推诿、扯皮最后甚至动武!)
- 三是没有足够的时间和动力来让我意识到可行和更好的区别以及“我从没见过比我设计得更优秀的所以我的理所应当就是最好的”。
问题
接下来我们来看看下面的几个问题你是否也感同身受?
-
有的API从文档一眼看上去满屏的参数,注释或说明几乎没有,用法、场景、异常处理等只字未提。如果是新人接手则完全不知道这些是干什么用的,只能通过口头询问或者翻看源码才能勉强找到答案。
-
有的API风格不统一,各个业务间,甚至同一业务内命名、入参、出参、错误信息各有不同。合理的解释是因为不同的API出自不同的工程师之手,我来接手这份代码的时候就已经是这个样子了。
-
接口响应时间突然从几十毫秒就变成了几十秒甚至超时;报错抛出的是一大串英文;API性能指标更无从谈起,我也不关注这块因为我们大部分业务都是toB的,根本就没有高并发的实际情况。
-
随着业务的演进,开放的API持续在增加,但类同的很多,有时候查询同一个内容有多个接口,干的却是一个事情。原因可能是完全不知道已经有同样功能的接口;或是别人写的接口我可不敢乱动,万一改坏了该怎么办呢?所以保险起见我还是新起一个接口吧!
以上几个问题可能每天都发生在我们的身边,但我们早已习惯麻木,直到AI编程的出现,它将会毫不留情的指出我们的缺点,激进的对我们的代码进行重构,写出具有非常优秀特质的API,最后取代我们的工作干碎我们的饭碗!听起来太可怕了,那么我们还是了解一下什么是优秀API的特质吧:
优秀的API应该具有易用性、可扩展性、安全性、稳定性、兼容性、文档化、可测试性和良好的性能和效率等特质,以满足开发人员和客户的需求。
这些特质我们文字上都看得懂,但很多同学还是不知道要怎么做,那API设计都有哪些原则呢?
API设计应该遵循简洁性、一致性、可预测性、可扩展性、安全性、高性能、可伸缩性、可测试性、可文档化和面向资源等原则,以提供易用、可靠、高效和安全的API服务。
通过这些文字,我们意识到原来我们工作中开发API接口很多都没有遵循这些原则,例如:简洁性、可文档化对应问题1,一致性、可预测性对应问题2,高性能、可伸缩性对应问题3,可扩展性对应问题4。
原则是死的,人是活的,我们还没进化到完全通过AI智能写代码的程度,尽管那是趋势,但我很好奇当AI读到我们的屎山代码的时候,它会发出怎样的感叹!或者当我们用大量的屎山代码库去训练AI时,它会不会写的比我们更好?
分析
接下来我在API接口设计阶段的思路和方法上进行举例和总结,重点用粗体表示,可能有失偏颇的地方还请各位批评指正。
1、动手的理由是什么
不是随便一个功能就要有个接口,也不是随便一个需求就要加个接口。
每新建一个接口,要有充分的理由和考虑,即这个接口的存在是十分有意义和价值的。无意义的接口不仅增加了维护的难度,更重要是对于程序的可控性的大大降低,接口也会十分臃肿。
2、从取个优雅的名字开始
设计接口时,分析的角度要统一。否则会造成接口结构的混乱。每个API接口应该只专注一件事,并做好。产品概念简单、关系清楚。功能模棱两可,诸多特殊逻辑的API肯定不是个优雅的API,且会造成功能类似重复的API。
注意:如果API它很难命名,那么这或许是个不好的征兆,好的名称可以驱动开发、并且只需拆分与合并模块即可。
避免:/api/getdata、/api/servicexyz、/api/action1、/api/action2、/api/action3
推荐:get('/api/users/:id/orders')、put('/api/users/:id')、post('/api/users')、delete('/api/users/:id')
功能大而全的API在灵活性、简单性方面肯定捉襟见肘。定义API的粒度之前,建议先将业务分领域、划边界,以此来提取业务对象,然后再根据业务对象用例来设计单一功能的API。
3、精简参数和参数收拢
接口设计简单、清晰。API执行的功能可以很丰富、很强大,但API声明和用法一定要尽量的简单,不能将功能的丰富通过复杂的用法来实现,这会导致API功能不单一,演进不可控。
- 你是不是强迫调用方关注/提供他们不在乎的选项/配置?
- 有没有毫无价值的额外步骤?
代码举例:
function generatePDF(html, orientation = "portrait", format = "A4", margin = "0.5in") {// 根据给定的HTML生成PDF文件,可以指定方向、格式和边距...
}
在这个示例中,generatePDF函数接受四个参数,其中三个(方向、格式和边距)是可选的。但是,这个函数的问题在于,它将这三个选项强制添加到了函数签名中,这意味着调用方必须关注这些选项,即使他们对这些选项并不在乎或根本不需要它们。
这个问题的解决方法是将选项封装在一个对象中,并将其作为单个参数传递,,例如generatePDF(html, options)
。这样,调用方可以只关注他们需要的选项,从而提高代码的可读性和可维护性。
此外,如果将来需要添加更多的选项,可以简单地在单个对象中添加新的属性,而不需要更改函数的签名。这提高了代码的可扩展性。
4、数据模型!==接口入参/出参
API的入参、出参所述的对象、属性,一定是按业务特性进行抽象后的实体,而不要误将底层数据模型概念如实的反应到API上。抽象API、抽象对象实体更宏观,具有更好的适用性、兼容性、扩展性。
// 错误的代码
function createUser(username, password, role_id) {// ...
}
// 底层数据模型
class User {constructor(username, password, role_id) {// ...}
}
在这个示例中,API使用了底层数据模型的概念来定义createUser
函数的参数,即username
、password
和role_id
。这使得API难以使用和难以维护,因为调用者可能不知道如何使用这些参数,并且将来更改底层数据模型时,这些参数也需要更改。
// 更好的代码
function createUser(user) {// ...
}
// 更好的底层数据模型
class User {constructor(username, password, role) {// ...}
}
这样就可以将API的参数定义为一个包含用户信息的对象,这样调用者就可以更容易地使用API,并且在底层数据模型更改时,也可以更轻松地更新API。
5、减少受“惊”的频率
API代码应该尽可能减少让读者惊讶甚至惊吓,你是否有过看到一堆或某行奇葩的代码时发出“我艹”惊呼的经历?惊为天人在程序员的世界里是个贬义词。业务API只需根据需求来设计即可,不需要刻意去设计一下复杂无用、华而不实的API,以免弄巧成拙。
在关键的处理逻辑或条件判断时添加更多的注释、错误处理和文档,可以帮助调用者正确地使用这个接口,也有助于提醒作者经过一段时间后自己都忘记掉的一些技术细节。你永远不知道下一个来使用你的代码的人是谁,朴实无华的写上接口说明和注释,减少将来后继者感叹的可能和不远万里找到你“问东问西”的频率。
下面是ChatGPT聊天的API接口文档,可以看到接口是中规中矩、平平无奇甚至有点简陋到随便一个初级开发就能够写出来,model+messages
两个必传参数就已满足需求,但接口背后的技术似乎正在引领一场技术革命。让人惊讶的不是接口本身,而是接口能够提供的能力。
6、降低耦合
API应该减少对其他业务代码的依赖关系。低耦合往往是完美结构系统和优秀设计的标志。
耦合的种类:
- 代码实现业务逆向调用。(利用面向对象编程三大特性-封装、继承、多态)
- 条件逻辑依赖耦合。(使用策略模式或表驱动方法来重构代码)
- 耦合API无关的业务行为。(提取单独的函数或类)
7、测试是我们的好朋友,不是工具人
对于API调用者而言,API应该是可被测试且易于被测试的。测试API不需要依赖额外的环境、容器、配置、公共服务等。
对可测试友好的API也是可被有效集成测试的前提。另外单元测试、集成测试和自动化测试工具也很重要,测试用例很可能我们都听过但都没写过,但这都不重要,重要的是接口写完了你总得试着执行一下吧?啥都不管直接丢给使用方/测试方真的合适吗?遇到没充分自测的接口,使用方/测试方的内心活动是:二营长!你他娘的意大利炮呢?给我拉来!
重要的事情说三遍:“一定要接口自测!一定要接口自测!一定要接口自测!”
8、统一大业
API要具备统一的命名、统一的入/出参规范、统一的异常规范、统一的错误码规范、统一的版本规范等。
统一规范的API优点:
- 易于被框架集成、处理
- 有助于API调用方、API提供方开发经验复用
- 避免犯错,避免误用
同志们,任务仍很艰巨,让我们一起来完成统一大业吧!
以上8条不仅适用于前端和后端之间的API接口,也同样适应于前端与前端之间、后端与后端之间。
思考
其实以上提到的这些示例、经验和教训网络上一搜也是一大把,作为信息技术如此发达的时代,想要找到一些关于接口设计方面的内容可以说是轻而易举,但为什么人们就是不喜欢遵循API设计原则来进行API接口开发呢?
有一些可能的原因,导致人们不喜欢遵循API设计原则来进行API接口开发:
-
缺乏经验或知识:一些开发人员可能缺乏API设计的经验或知识,或者没有了解API设计原则的重要性。在这种情况下,他们可能会编写低质量的API,而不是遵循最佳实践。
-
时间和成本压力:有时候,开发人员可能会面临时间和成本的压力,需要尽快完成API开发。在这种情况下,他们可能会牺牲API设计原则的质量,以满足短期目标。
-
不了解API的受众:开发人员可能会忽视API的受众,即使用API的其他开发人员。他们可能会觉得自己编写的API是最好的,而不考虑其他人的需求和使用方式。
-
公司文化和管理:一些公司可能没有重视API设计原则,或者没有提供相关的培训和支持。在这种情况下,开发人员可能会缺乏对API设计原则的了解和实践,从而导致API的质量不佳。
总的来说,遵循API设计原则可以提高API的质量和可维护性,但是这需要开发人员有一定的经验和知识,并且需要在公司文化和管理上得到支持和重视。
缺乏经验或知识、不了解API的受众这两点是可以通过主观能动性来解决的,但时间和成本压力、公司文化和管理则不受开发人员的意志为转移。
总结
好吧,如果有人能够耐心的看到结尾这里,一定是一个技术觉悟非常高的人!
总结一句话:没有经过设计的接口是没有灵魂的!