9 月 13 日,2025 Inclusion・外滩大会「开源嘉年华」正在限量报名中! 了解详情
写点什么

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:462194
用户头像

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

关注

评论

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

泛互联网行业A/B测试全解析:产品优化的创新之道

字节跳动数据平台

A/B 测试

ClickHouse分析效率翻倍提升,揭秘奇点云对归因分析场景的优化实践

先锋IT

华为云DTSE助力无锡云数IoT系统:打造超可靠数字化之源

华为云开发者联盟

云计算 后端 华为云 华为云开发者联盟 华为云DTSE

百度基于金融场景构建高实时、高可用的分布式数据传输系统的技术实践

JackJiang

网络编程 即时通讯 IM

国外服务器租用:如何在预算内选择最优服务

一只扑棱蛾子

国外服务器

详解KubeEdge EdgeMesh v1.15 边缘CNI特性

华为云开发者联盟

开发 华为云 容器网络 华为云开发者联盟

微信公众号短链实时获取阅读量、点赞数爬虫方案(不会Hook可用)

不在线第一只蜗牛

爬虫 微信公众号 工具分享 hook

一款轻量级、基于Java语言开发的低代码开发框架,开箱即用!

互联网工科生

Java 低代码 数字化 开发框架 JNPF

阿里云PolarDB开发者大会首度召开,让数据库开发像“搭积木”一样简单

阿里云瑶池数据库

数据库 云计算 阿里云 云原生 开发者大会

阿里云容器服务助力万兴科技 AIGC 应用加速

阿里巴巴云原生

阿里云 云原生 容器服务

Web Components从技术解析到生态应用个人心得指北

zhoulujun

克魔助手抓包教程:网络数据包分析利器

喜报!博睿数据荣获数据猿“年度创新服务企业奖、年度创新服务产品奖”

博睿数据

可观测性 博睿数据 运维监控

“一次不过、免费再考” 限时活动开启,快来考取亚马逊云科技认证吧!

亚马逊云科技 (Amazon Web Services)

培训与认证

智能开发助手——华为云CodeArts Snap,揭开智能研发新篇章

人工智能 华为云

克服传统企业数字化转型的十大阻力

天津汇柏科技有限公司

数字化转型

一、nextjs如何使项目工程化(c-shopping电商开源)

Geek_9da61c

开源 eslint prettier husky next.js

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