良好 API 设计的实用建议

2025-05-25

良好 API 设计的实用建议

在一次难得的 YouTube 学习中,我偶然看到了 Joshua Bloch(《Effective Java》的作者)的演讲,主题是如何构建优秀的 API 以及它的重要性。看完之后,我知道自己必须记下来,因为这个演讲太精彩了,让我难以忘怀。事实上,它精彩到我都想和大家分享。

Joshua 成功地在一个小时内讲解了多个主题,涵盖了优秀 API 的高级特性、构建 API 的流程以及一些实用技巧。那么,让我们开始吧。

首先,我们来快速了解一下什么是 API。视频中没有涉及 API 的内容,所以如果你已经了解 API,可以跳过这部分。

API(应用程序编程接口)可以被视为与 API 背后的软件进行通信的契约。它定义了您可以获取哪些数据、数据格式以及可以对这些数据执行哪些操作。这意味着 API 可以是任何形式,从成熟的 REST API 到一组可以调用来操作列表的方法。

根据 Bloch 的说法,设计一个好的 API 可以参考某些特性。

良好 API 的特征

  • 易于学习
  • 易于使用,即使没有文档
  • 难以滥用
  • 易于阅读和维护使用它的代码
  • 足够强大,满足要求
  • 易于进化
  • 适合观众

虽然这些特征相当抽象且难以实现,但它们可以作为指导方针。如何实现这些特征是本文的其余部分要讨论的。

构建 API 的过程

构建 API 的第一步是从需求入手。但是,要谨慎对待利益相关者提出的解决方案,并尝试提取用例。要弄清楚您要解决的具体问题,而不是用户希望如何解决。

一旦你确定了需求,就从小事做起。写一份最多一页纸的说明书。

任何超过这个数字的事情,你的自尊心都会被投入其中。沉没成本谬误就会出现,你不会愿意放弃它。

修改起来很省力,而且一旦收到反馈,重写起来也很容易。只有当你开始更好地理解你试图解决的问题时,你才应该进一步充实规范。

虽然听起来有点违反直觉,但你应该立即开始针对你的 API 进行编码。创建接口,在确定所有规范之前,不要考虑具体实现。即使确定了所有规范,也要持续针对你的 API 进行编码,以确保其行为符合你的预期。这能让你理清思路,避免意外。

这些代码片段可能是您为 API 编写的最重要的代码之一。它们可以作为示例,您应该在这些代码片段上投入大量时间。一旦您的 API 投入使用,示例代码就会被复制。拥有好的示例意味着您的 API 能够得到良好的使用,因此它们应该具有示范性。

然而,构建 API 时最重要的是

如果有疑问,就将其省略。

特别是如果您正在构建公共 API,一旦用户开始使用它,就几乎不可能删除它。

实用技巧

注意,这些例子很多都是基于 Java 和 OOP(面向对象编程)。不过,大部分内容在 Java 和 OOP 之外仍然适用。

API 应该只做一件事,并且要做好。
功能应该易于解释。如果难以命名,通常不是一个好兆头。好的 API 应该读起来像散文一样流畅。
如果想在一个地方做太多事情,可以考虑拆分;如果想做类似的事情,可以考虑将它们放在一起。

API 应该尽可能精简,但不能更小。
满足需求,其他一切都抛在一边。你可以添加,但不能删除。
考虑一下理解 API 需要学习的概念数量。你应该考虑学习 API 的概念权重,并尽量将其保持在最低水平。
一种方法是尽可能地复用接口。通过复用接口,用户只需学习一次该接口。

不要将实现细节暴露在 API 中
。您不应该将实现细节暴露给客户端。如果您想更改实现,这会使 API 更难更改。
一个例子就是抛出异常。您可能抛出了 SQL 异常,但在更高版本中还想实现另一种数据存储形式。现在,即使您尝试写入文件,也必须抛出 SQL 异常,因为用户正在等待并处理 SQL 异常。

尽量减少所有内容的可访问性,
尽可能地确保其私密性。这样您就可以灵活地更改名称和实现,而不会影响客户端的实现。

名称至关重要。
名称应该尽可能地不言自明,你应该将 API 视为一种小型语言。这意味着它的命名应该保持一致。相同的词语应该表示相同的事物,并且应该使用相同的含义来描述相同的事物。

// Does the same thing, but different names are used
fun remove()
fun delete()
Enter fullscreen mode Exit fullscreen mode

文档至关重要。API
中,文档完善的组件更有可能被复用。务必认真记录,尤其是在处理状态或副作用时。文档越完善,用户遇到的错误就越少。

切勿为了性能而扭曲 API
。良好的 API 设计通常与良好的性能相辅相成。例如,将类型设置为可变类型或使用实现类型而非接口,可能会限制性能。
为了获得更好的性能而扭曲 API,可能会破坏 API 本身。例如,将不可变类设置为可变类,以减少内存占用。虽然底层的性能问题会得到解决,但令人头疼的问题却会永远存在。

最小化可变性
除非有充分理由,否则类应该是不可变的。如果必须可变,请尽可能缩小状态空间。

仅在有意义的情况下
使用子类。仅当您能直截了当地说子类的每个实例都是超类的实例时才使用子类。如果答案不是肯定的,请改用组合。暴露的类不应该仅仅为了重用实现代码而使用子类。

为继承进行设计和文档化,否则就禁止继承
。这适用于面向对象编程 (OOP)。避免“脆弱的基类问题”,即对基类的更改可能会破坏子类的实现。
如果无法避免,请详细记录方法之间的相互调用方式。尽管如此,还是尽可能限制对实例变量的访问,并使用 getter 和 setter 来控制基类的实现。

不要让客户端做任何模块可以做的事情,
让 API 去做那些总是需要做的事情。避免客户端使用样板代码。

// DON'T
val circle = CircleFactory.newInstance().newCircle()
circle.radius(1)
circle.draw()
// DO
val circle = CircleFactory.newCircle(radius = 0.5)
circle.draw()
Enter fullscreen mode Exit fullscreen mode

应用最小惊讶原则
API 用户不应该对行为感到惊讶。要么避免副作用,要么使用描述性名称来描述副作用。

快速失败
错误发生后应尽快报告。编译时报告是最佳选择,因此请充分利用泛型/静态类型。

提供对所有字符串形式的可用数据的编程访问。
如果您只使用字符串,格式和内容将成为 API 的一部分,因此您永远无法更改它。因此,请通过对象提供对字符串内容的访问。这样,您无需对字符串的格式和内容做出任何承诺。

谨慎重载
仅当方法行为相同时才重载。以 Ja​​va TreeSet 构造函数为例,TreeSet(Collection) 忽略顺序,而 TreeSet(SortedSet) 则遵循顺序。

使用合适的参数和返回类型。
输入时优先使用接口而不是类,但要使用最具体的输入参数类型。如果存在更好的类型,请勿使用字符串。例如,货币值也不应该使用双精度或浮点数。

跨方法使用一致的参数顺序,
特别是当参数类型相同时,因为您可能会意外地交换参数。

fun copy(source: String, destination: String)
fun partialCopy(destination: String, source: String, numberToCopy: Int)
Enter fullscreen mode Exit fullscreen mode

避免使用过长的参数列表
。理想情况下,参数列表应不超过三个。过多的相同类型参数可能有害,并且容易出错。如有必要,请拆分函数或使用辅助类来保存参数。

避免返回需要异常处理的类型。
用户可能会忘记编写特殊情况的代码,这可能会导致错误。在非异常流程足够的情况下,应避免这种情况。例如,返回零长度数组或集合,而不是空值。


最后,你应该预料到会犯错误,这就是为什么这些观点中有很多是关于能够轻松地改变事物,而不是从一开始就构建完美的 API。

也请去看看他的演讲,他讲得比我这里详细得多。
希望你觉得这些内容有用,也欢迎在评论区分享你的想法或经验!
感谢阅读❤️

资源:

如何设计一个好的 API 以及它为何重要- Joshua Bloch(幻灯片

文章来源:https://dev.to/johannea/practical-advice-to-good-api-design-2hac
PREV
使用 Windows Subsystem for Linux 的 Epic 开发环境
NEXT
迷你指南 - 与 MySQL 一起构建 REST API 作为 Go 微服务设置 API 数据库连接结构和依赖关系数据库迁移包装 REST API