取消

好的框架需要好的 API 设计 —— API 设计的六个原则

说到框架设计,打心底都会觉得很大很宽泛,而 API 设计是框架设计中的重要组成部分。相比于有很多大佬都认可的面向对象的六大原则23 种常见的设计模式来说,API 设计确实缺少行业公认的原则或者说设计范式。

不过,没有公认不代表没有。无论是对外提供类库还是提供 url 形式的 API,为了使用者良好的使用体验,依然也是有可以借鉴和参考的经验的。


This post is written in multiple languages. Please select yours:

本文中的 API 设计原则在主要思想上出自 NetBeans 创始人 Jaroslav Tulach 所著的 Practical API Design 一书;但原书讲述的所有内容很零散,缺乏系统性。所以我们结合了一些开源项目的 API 升级方式对内容进行了整理,形成六个原则。

API 是什么?

如果要解释 API 这个英文缩写,那一定要说出它的英文原文来:Application Programming Interface,即应用编程接口。虽然维基百科上有它的定义,不过还是太复杂了。

在 .NET 中,我们认为 API 包括了所有公开的类、接口、属性、字段、方法,以及类库提供的配置文件(包括格式)、协议等。

API 设计原则

即便没有学习过任何 API 设计,也没有阅读过设计或重构相关的书籍,只要你有一些编程经验,应该都能够或多或少地评估一组 API 设计得是好是坏。因为——我们都是 API 的使用者,用的 API 多了,也便能体会到各种不同 API 带给我们的不同体验。

我们团队的几个小伙伴开撕之后,写出了以下文辞:

我(吕毅)说:
在 API 设计原则里面,无法写出错误代码是为上策,写错了会出现异常次之,仅靠文档约束为下策,连约束都没有只剩坑的需祭天。

头像大人云:
故上码伐编译,其次伐异常,其次伐文档,其下祭天。祭天之法,为不得已。

所以,在下面总结的 API 设计原则中,前面四个都是站在使用者的角度来考虑的。

可理解性

通常使用者希望使用到某个 API 的时候,为了正确使用这个 API,需要学习一些与这个 API 相关的新知识。而需要新学习的知识越多,我们认为“可理解性”就越低。

为了提升 API 的可理解性,我们在设计 API 的时候建议考虑这些因素:

  1. 如果没有必要,不要引入新的概念
  2. 防止误用
    • 最好能够避免使用者写出错误的代码(即让错误的代码编译不通过)
    • 如果上面那一条有些难度,则建议在运行时抛出异常(使用者便能够明白为什么自己写错了,改怎么更正)
    • 另外,最好让错误使用的代码变丑(例如非常冗长难以理解,例如 IDE 会显示下划线警告)
    • 不要试图在文档中警告使用者用错了,因为典型的程序员是不看文档的

关于防止误用的一个优秀案例,要属单元测试模拟 Moq 了;可以参考 Moq 基础系列教程 并上手编写,体验它对防止误用上做出的努力。

可见性

我们大多数人的开发工具是功能齐全,傻瓜也能使用的 IDE(集成开发环境),这其实是 IDE 可理解性较好的一个体现。

不过这里要说的是 IDE 的智能感知提示功能;就算没有 IDE,一些常见的代码编辑工具(Visual Studio Code、Sublime、Atom、Notepad++、Vim)也都带有只能感知提示功能。在智能感知提示的帮助下,我们能够在不查阅文档的情况之下了解到当前上下文相关的 API 说明及其简易的使用提示。

如果我们只通过智能感知提示便能够发现一个新 API 并正确使用它,便可以说这个 API 的可见性是好的。

典型的例子是实现或者调用某个函数过程:

  1. 实现某个函数的时候,函数的参数类型本来并没有见过,但通过智能感知提示我们能够了解到这个新 API 并正确取到参数中我们期望得到的信息。
  2. 调用某个函数的时候,我们需要传入本来并没有见过的参数类型,通过智能感知提示,我们能够知道如何构造或获取这些类型然后正确传进去。
  3. 调用完某个函数后我们得到了返回值,我们本来并没有见过这个类型,但通过智能感知提示,我们能够学习到这个新的类型,并知道如何正确使用这个返回值。

如果画一个图来表示较高的可见性和较低的可见性,我想可以画成这样:

可见性
▲ 连接线表示可以通过函数的参数、返回值等得知的新 API

左侧的 API 没有什么规律,知道什么或者不知道什么全凭经验而定。右侧的 API 从入门 API 开始,可以发现可见性较高的其他相关 API;当更深入地使用后,可能可以发现更高级别(通常也更难正确使用)的 API。

当然,并不是说可见性越高越好,如果某些 API 是用来完成某些高级功能,或者这个 API 存在较大的性能开销等,为了避免初学者混淆或者误用,应该适当降低其可见性。

为了更好的可见性,简易在 API 设计的时候:

  1. 对于多数常用功能,尽量少提供独立的类;
  2. 对于高级功能,尽量与简单功能隔离。

一致性

当多个相似功能的 API 之间有相似的使用方法时,使用者只需要很少的迁移成本便可以轻松学会新 API 的正确用法。

比如 LINQ 带来了集合的便捷操作,其中的 Select 方法用于查找和转换集合每一项的信息。而 LINQ to XML 虽然不是在操作集合而是在操作 XML,但其也有 Select 等方法完成节点的查找和选择。于是,使用者可以通过智能感知提示大致了解到 Select/SelectSingleNode 的基本正确用法。这便是良好的一致性带来的快速入门体验。

简单性

可能有些 API 在经过修改满足了以上可理解性、可见性、一致性之后,极有可能导致一个类或者一组相关类包含了太多方法可用。于是,简单而正确的使用可能就隐藏在众多的 API 中。当然,从面向对象的原则中我们可以说这通常违反了“单一职责原则”。

简单的任务应该有简单的实现,这是 API 设计中简单性应该做到的。这意味着 API 在提供了灵活的功能之后,建议为常用的任务提供更简单的调用方式。

例如,InkCanvas 只需要添加下面这样的 XAML 便可完成书写功能:

1
<InkCanvas x:Name="inkCanvas" />

虽然可以进行更多的定制,但是这不是必须的,更多的定制是属于更高级的功能需求的:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 以下源码来自 https://docs.microsoft.com/en-us/windows/uwp/design/input/pen-and-stylus-interactions

 // Set supported inking device types.
inkCanvas.InkPresenter.InputDeviceTypes =
    Windows.UI.Core.CoreInputDeviceTypes.Mouse |
    Windows.UI.Core.CoreInputDeviceTypes.Pen;

// Set initial ink stroke attributes.
InkDrawingAttributes drawingAttributes = new InkDrawingAttributes();
drawingAttributes.Color = Windows.UI.Colors.Black;
drawingAttributes.IgnorePressure = false;
drawingAttributes.FitToCurve = true;
inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttributes);

可测性

API 内部本身需要被测试(单元测试、基准测试等);然而,API 的使用者也应该具备可测性。

典型的反例,比如获取某个配置文件的配置信息的方法是静态方法 Config.Get("SomeKey")。那么使用这个 API 的开发者就很难写出能够被单元测试的方法,因为找不到有效的方案来模拟这样的静态方法。

兼容性

良好的 API 设计利于未来的版本升级——升级带来的用户兼容性成本较低,或者框架开发者的兼容性包袱较轻。

兼容性有三类:

  • 二进制兼容:更新库后,无需重新编译项目,能够直接运行而不会崩溃。
  • 源码兼容:更新库后,可以不用修改项目的源代码可编译通过。
  • 功能兼容:更新库后,功能表现依旧和更新之前一样。

为了将来的兼容性考虑,设计 API 时建议考虑这些因素:

  1. 不要提前公开 API
    • 如果你的某个 API 是为将来预留的,那么不要开放,因为你不清楚未来的设计需求是怎样的,提前公开的 API 在将来改变的可能性非常高)
  2. 预留足够的扩展点
    • 没有良好扩展性的 API 通常会因为频繁的需求变更而导致 API 间接变化,这都是兼容性成本。如果在良好的设计下预留了足够的扩展点,那么这样的 API 能够应对未来一段时间内未知的需求变化,使得 API 变化在可控范围内。
    • 要预留扩展点就意味着通常应该使用接口或者抽象的概念来描述 API,建议用清晰定位的接口替代具体的类型。
  3. 应该有明确的 API 迁移说明
    • 如果某个 API 过时了,也不建议删除它;应该标记为过时,并告诉使用者新的 API 是什么。当然如果这个 API 会导致出现不可接受的问题,也可以标记它无法通过编译。

框架设计

Practical API Design 一书认为框架和 API 是等同的。不过从实际行业上的描述来看,框架是更大层面的 API,可以理解为用于完整解决某类问题而开发的一整套 API。

框架的概念可以很大,也可以很小。Avalonia 可以称为一个跨平台的 UI 框架,这是很大的框架;其中的 ReactiveUI 是一个 UI 响应框架(包含 MVVM)。更小的可以有一套多语言框架、一套依赖注入框架等。

实践以上总结的六个原则,我们也许能设计出更多优秀的框架。


参考资料

  • Practical API Design, NetBeans 创始人 Jaroslav Tulach 著

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/framework-api-design.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected])

登录 GitHub 账号进行评论