2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

API 设计的“不可承受之轻”

  • 2007-09-03
  • 本文字数:3139 字

    阅读完需:约 10 分钟

API 的设计影响着所有的开发者。有些 API 用起来很舒服,而有些则用起来让人焦头烂额,更有甚者,让人完全丧失了继续用这套 API 来做开发的勇气。但它们的区别在哪里呢?是哪种品质会让一套 API 易用而另一套复杂难解?ACM Queue 最近刚发布了 Michi Henning 的一篇有关 API 设计的文章,作者在文中剖析了 API 的好坏之分,并指出了 API 对生产力的影响。

当我们开始用 API 的时候,我们就能知道它是好是坏。好的 API 会让人心情愉悦,用起来得心应手:当你想调用某个特定的任务时,最合适的方法会在最恰当的时候出现在你面前,容易发现,易于记忆,文档说明详尽,接口设计符合人的思维习惯,而且正确的处理了边界条件。 那为什么现在还有这么多垃圾 API 呢?其主要原因在于,对于每一种正确的 API 设计方式而言,都同时伴随有很多种错误的设计方式。简单来说,设计一种坏的 API 要比设计好的 API 容易的多。即使是很微不足道的、无伤大雅的设计上的小小缺陷,最终也会被放大成巨大的阴影,因为 API 只需要提供一次,但是却会被调用很多次。如果一个设计缺陷会导致笨拙或是低效的代码,那么这个问题就会在 API 每次被调用的时候都显现出来。而且,有些设计缺陷单独看起来很小,但是却会以令人讶异的破坏性的方式来相互影响,从而很快就会导致不计其数的间接性破坏。

Michi 在给出自己对 API 设计的建议之前,先举了一个反例为证。他对.NET 框架中的 Select() 方法调用进行了分析,展示了很多 API 中都存在的共通问题。

Select() 方法中传入的参数是需要进行监控的 socket 列表。在大多数应用程序中,所使用的 socket 不会常常变化,所以这些列表基本上在很长一段时间内都是不变的。但是:

因为 Select() 方法对参数进行了重写,所以调用者在把每一个列表传入 Select() 方法之前必须要保留一个备份。这就给用户带来了很大的不便,而且不宜扩展应用的规模:服务器常常都要监控上百个 socket,而在每一次循环中,调用 Select() 方法前都要复制所有的列表。

因为 socket 会进入等待或是阻塞状态,所以 Select() 还接受一个时间参数用来标识超时,如果在参数所指定的时间内没有任何 socket 可用,那么方法就会返回。但是,因为这方法是 void 类型,所以没有很便捷的方式可以标明 Select() 方法是因为有了可用的 socket 而返回,还是因为超时返回的。

为了判断是否有可用的 socket,调用者必须要对方法参数中的三个列表各自的长度进行测试;如果三个列表长度都是零,那么就是没有可用的 socket。如果调用者碰巧很关心这一点的话,那就要写一个相当别扭的测试了。更有甚者,如果没有 socket 就绪而发生超时,那么该方法就会销毁调用者传入的参数,所以就算是什么事情都没有发生,调用者还是必须要在每一个循环中对这三个列表进行备份!

而且,在 API 文档中,也没有明确的指出该怎样无限期地等待 socket 就绪。Michi 还是通过自己的试验才发现,如果超时参数为 0 或是负数,那么该方法就会立刻返回,而且也没有任何办法可以让该方法等待的时间超过 35 分钟。这就逼着他只好自己实现了一个 Wrapper,来完成持续性等待的功能。

在写这个 Wrapper 的时候,Michi 又发现了 API 设计中新的潜在问题:

Select() 方法的另一个问题就是它接受的参数是 socket 列表。列表中可以允许同一个 socket 多次出现,但是这样做一点意义都没有:从概念上讲,传入的应该是由 socket 组成的 set。那么为什么 Select() 会使用列表(List)呢?原因很简单:.NET 容器类中没有包括对 set 的抽象。使用 IList 来对 set 建模是让人徒呼奈何的一件事情:它会带来语义上的冲突,因为 list 允许出现重复的元素。(Select() 方法遇到重复的 socket 会发生什么样的行为大家只能瞎猜,因为没有明确的文档记录;而要实际去检测它的真实行为也是没有意义的,没有文档说明,我们怎么知道实现会不会在什么时候突然改变?)

一个劣质的 API 设计所带来的问题就是,差不多每个用这套 API 的开发人员都要设法去弥补其中的缺陷。该 API 的用户群越大,开发人员浪费的时间也就越多。

劣质的 API 不但会带来更多的编码,而且代码结构也会更加复杂,潜藏 bug 的地方也就越多。

不过设计错误所发生的位置不同,其后果也会有所差异。

在抽象层次中,如果 API 缺陷出现的位置越低,其后果也就越严重。如果我在自己的代码中错误地设计了一个函数,那么影响到的人就只有我自己而已,因为我是这个函数的唯一调用者。如果我在我们的项目库中错误地设计了一个函数,那么或许所有的同事都会遭罪。如果我在一个广泛采用的代码库中错误的设计了一个函数,那么或许就会有成千上万的程序员开始诅咒了。

任何大面积流行的公共代码库或多或少都会被认为是不可变的。这种类型的 API 的任何变化都有可能破坏向后的兼容性,引起大量问题。方法声明的变化会导致客户端无法编译或是崩溃,方法行为的变化会导致无可预计的微妙错误。即使是修复 bug 也会有问题,因为会有些既有的代码是依赖于原始的有 bug 的行为的。

在动态链接库的世界中,客户端代码就更容易受到 API 变化的影响了。哪怕采用了版本机制,客户端代码又怎样判断库文件版本号的小小变化是否会影响到当前任何对 API 行为的假定呢?

即使 API 自身的变化会让框架变得更好,但是每一个用户也都要为升级付出一定的代价。当然,也有的变化不会引起客户端哪怕一行代码的改动,可就算这样,每一个客户端也不得不进行重新编译,以防止出现崩溃的情况,因为源码不变的情况下,二进制码也会发生变化。

一个API 设计者需要考虑很多事情,当然,大多数的决定都是根据过往经验做出的,不过还是有些比较优秀的想法应当推荐给大家。Joshua Bloch 在 Javapolis/InfoQ 上关于 API 的演讲中和听众分享了自己的心得,而在他所有的建议中,最根本的一条就是要让 API 尽可能地小(但绝不过分小),因为:

你可以随时往里面加东西,但是你永远不能删除些什么。

Joshua 讨论了几乎每一件应当注意的事项,从不可变性(保证类的不可变,除非有比较好的理由要求可变)等概念的重要性,到比较细微但也同样重要的细节问题,比如只要在可能的情况下,就宁可返回空的列表也不要返回 null 值(因为返回一个 null 值会导致调用者要做多余的检查)。

Michi Henning 同样在文中也给出了他的一些如何让 API 更加优秀的建议:

  • API 必须要提供充分的功能,以供调用者完成自己的任务。
  • API 应该是最精简的,不要为调用者带来多余的不便。
  • 如果没有理解 API 的使用环境的话,那也就不能去设计它。
  • 通用性的 API 应当是与具体使用场景无关的,而特定用途的 API 则要充分考虑使用策略。
  • API 应该从调用者的角度来进行设计。
  • 好的 API 绝不推卸责任,把自己该做的事情留给别人。
  • 在实现 API 之前,就应该把 API 文档化。
  • 好的 API 应当符合工效学(Ergonomic)。

因为大多数人都会承认这些建议的重要性,人们会觉得应该在学校里教授这些知识,但是看起来这还不属于当前的标准教学过程。Ed 描述了现在的课程中这些知识的欠缺,作为对 Michi 的文章的评论

我现在意识到了我所接受的教育是不完整的。我上的课只是教会了我们怎样寻找最好的算法,或是和旧的数据结构行为相似的新数据结构(还有人记得双向队列么?)。而且我们的作业也从来没有根据设计来评分:评分的依据纯粹是执行速度。

但是如果教育系统不教给学生如何进行 API 设计的话,那么资深的同事会教么?Michi 在这一点上不甚乐观:

经验的积累是需要时间的,而且还要经历过一点“吃一堑,长一智”,才能掌握如何把事情做得更好。但不幸的是,我们这个行业的趋势就是把经验最丰富的开发人员提升到远离编码的职位上,正好是在他们可以把多年积累的经验用到实处的时候。

这就让整个的学习过程变得很棘手了。那么,我们这整个行业该做出哪些改进呢?

查看英文原文: Why API design matters

2007-09-03 19:462276
用户头像

发布了 197 篇内容, 共 63.0 次阅读, 收获喜欢 21 次。

关注

评论

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

融云发布语聊房SDK 1.0 & Demo 满足开发者开箱即用需求

融云 RongCloud

酱香型白酒怎么选?唐庄酒告诉你答案

Geek_50a546

进击的云原生,为开发者提供更多可能性

阿里巴巴云原生

蚂蚁金服内部的Spring Cloud Alibaba手册,面面俱到,太全了

Java 程序员 架构 面试 微服务

如何做好业绩管理?

石云升

职场经验 管理经验 6月日更

网络攻防学习笔记 Day51

穿过生命散发芬芳

网络攻防 6月日更

数字人民币对地铁App的影响分析

CECBC

四川比特币矿机或全线关机,矿工跟矿机说:Bye,See you

CECBC

华为实习结束后,鹅厂和字节跳动递来的offer让我手足无措

Java 程序员 架构 面试

案例 | 低代码助力生产制造业,扫平数字化升级“死角”!

优秀

低代码 生产管理系统

Flink State 和 Fault Tolerance(四)

Alex🐒

flink flink1.13

零售的私域「留量」时代,SaaS的「服务」未来

ToB行业头条

Cilium 首次集成国内云服务,阿里云 ENI 被纳入新版本特性

阿里巴巴云原生

容器 运维 云原生

2021春招面试经历,阿里3轮技术面+交叉3面(已成功拿到offer)

Java 程序员 架构 面试

Swift在淘系的工程化应用和实践

阿里巴巴大淘宝技术

swift 大前端 编程语言 WWDC21

分布式事务框架seata落地实践

有道技术团队

分布式 大前端

从字节跳动到火山引擎(二):私有云 PaaS 场景下的 Kubernetes 集群部署实践

字节跳动 云原生 #Kubernetes# 火山引擎

从最难的而立之年走来,三十而已 —— 2021 年中总结

清秋

成长与思考 年中总结

百度大规模Service Mesh落地实践

百度开发者中心

百度 service

融云CTO任杰:强互动,RTC下一个“爆点”场景

融云 RongCloud

从字节跳动到火山引擎(三):替换 Spring Cloud,使用基于 Cloud Native 的服务治理

字节跳动 微服务 云原生 Spring Cloud 火山引擎

微信小游戏直播在Android端的跨进程渲染推流实践

JackJiang

微信 即时通讯 直播技术

线程与线程池的那些事之线程池篇(万字长文)

秦怀杂货店

线程 线程池 并发

美联储最新货币政策,将如何影响比特币市场?

CECBC

BZZ云算力挖矿系统开发案例

细节爆炸!阿里企业级Spring Security机密文档

Java 程序员 架构 面试 微服务

联邦计算在百度观星盘的实践

百度开发者中心

百度 联邦计算

我看JAVA 之 线程同步(上)

awen

Java synchronized 管程

珠宝正品溯源平台,区块链珠宝溯源方案

13530558032

区块链电子印章平台--加速政务数字化

13530558032

API设计的“不可承受之轻”_架构_Niclas Nilsson_InfoQ精选文章