好的框架需要好的 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 的时候建议考虑这些因素:

  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 便可完成书写功能:

<InkCanvas x:Name="inkCanvas" />

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

// 以下源码来自 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)。更小的可以有一套多语言框架、一套依赖注入框架等。

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


参考资料

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

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