API 的用户体验设计

编写代码很少仅仅是您和您的计算机之间的私事。代码不仅仅是为了机器而存在的;它还有人类用户。它应该易于阅读,供其他开发者使用、维护和构建。当开发者使用他们喜欢的工具,并且保持快乐和高效时,他们就能生产出更多更好的代码。不幸的是,开发者经常被他们的工具所困扰,对着晦涩难懂的错误信息咒骂,想知道为什么那个愚蠢的库不能按照他们的想法工作。我们的工具很有可能给我们带来痛苦,尤其是在软件工程这样复杂的领域。

用户体验 (UX) 应该成为应用程序编程接口 (API) 设计的核心。一个设计良好的 API,能让复杂的任务变得简单,它能比一个设计精妙的床头灯更能减少这个世界上很多痛苦。那么,为什么与家具设计相比,API 用户体验设计常常像是事后诸葛亮呢?为什么开发者中普遍缺乏设计文化呢?

keep the user in mind


部分原因仅仅是缺乏同理心。当你独自一人坐在电脑前编写代码时,未来的用户只是一个遥远的、抽象的概念。只有当你开始坐在你的用户旁边,看着他们在你的 API 中苦苦挣扎时,你才会意识到用户体验的重要性。而且,让我们面对现实吧,大多数 API 开发者从来没有这样做过。

另一个问题是我所说的“聪明工程师综合症”。程序员倾向于认为最终用户有足够的背景和上下文——因为他们自己有。但事实上,最终用户对你自己的 API 及其实现的了解只是你所知的冰山一角。此外,聪明的工程师往往会把他们构建的东西复杂化,因为他们可以轻松地处理复杂性。如果你不是特别聪明,或者你缺乏耐心,那么这个事实就会对你软件的复杂程度设置一个硬性限制——超过一定的程度,你根本无法让它工作,所以你只能放弃,从一个更简洁的方法重新开始。但聪明、有耐心的人呢?他们可以处理复杂性,他们会构建越来越丑陋的科学怪人,而这些怪物不知何故还能行走。这就会导致最糟糕的 API。

最后一个问题是,一些开发者强迫自己坚持使用对用户不友好的工具,因为他们认为额外的难度是一种荣誉的象征,认为设计周到的工具是“给菜鸟用的”。我在深度学习社区中一些更具毒性的部分看到了很多这种态度,在那里,大多数东西往往是时尚驱动的、肤浅的。但最终,这种自虐式的姿态是弄巧成拙的。从长远来看,好的设计会胜出,因为它能让它的使用者更有效率,更有影响力,因此传播速度比对用户不友好的设计更快。好的设计具有感染力。

像大多数事情一样,API 设计并不复杂,它只需要遵循一些基本规则。它们都源于一个基本原则:你应该关心你的用户。 所有用户。不仅仅是聪明的用户,也不仅仅是专家。始终以用户为中心。是的,包括那些迷茫的、缺乏上下文和耐心的新手用户。每一个设计决策都应该以用户为中心。

以下是我的 API 设计三原则。


1 - 精心设计端到端的 用户工作流程。

大多数 API 开发者关注的是原子方法,而不是整体工作流程。他们让用户根据他们提供的基本原语,通过不断尝试来摸索出端到端的工作流程。由此产生的用户体验往往是一长串的黑客行为,绕过了在单个方法层面上不可见的 technical 约束。

为了避免这种情况,首先要列出你的 API 将要涉及的最常见的工作流程。大多数人都会关心的用例。自己亲自去体验一下,并做好笔记。更好的做法是:观察一个新用户如何完成这些工作流程,并找出痛点。毫不留情地消除这些痛点。特别是

  • 你的工作流程应该与用户关心的特定领域概念紧密对应。 如果你正在设计一个制作汉堡的 API,它应该包含一些毫不意外的对象,比如“肉饼”、“奶酪”、“面包”、“烤架”等等。如果你正在设计一个深度学习 API,那么你的核心数据结构及其方法应该与熟悉该领域的人所使用的概念紧密对应:模型/网络、层、激活函数、优化器、损失函数、epoch 等等。
  • 理想情况下,任何 API 元素都不应该涉及实现细节。 你不希望普通用户去处理“primary_frame_fn”、“defaultGradeLevel”、“graph_hook”、“shardedVariableFactory”或“hash_scope”,因为这些都不是底层问题领域的的概念,它们是来自你内部实现选择的非常具体的概念。
  • 精心设计用户入门流程。 完全的新手如何才能找到使用你的工具解决他们的用例的最佳方法?准备好答案。确保你的入门资料与你的用户关心的事情紧密相关:不要教新手你的 API 是如何实现的,而是要教他们如何使用它来解决他们自己的问题。

2 - 减少用户的认知负荷。

在你设计的端到端工作流程中,始终努力减少用户为理解和记忆事物运作方式而投入的脑力劳动。你对用户的努力和关注要求越少,他们就能投入更多精力去解决他们的实际问题——而不是试图弄清楚如何使用这个或那个方法。特别是

  • 使用一致的命名和代码模式。 你的 API 命名约定应该在内部保持一致(如果你通常使用 num_* 前缀来表示计数,就不要在某些地方切换到 n_*),同时也应该与广泛认可的外部标准保持一致。例如,如果你正在为 Python 设计一个数值计算 API,它就不应该与每个人都在使用的 Numpy API 发生明显的冲突。一个对用户不友好的 API 会在 Numpy 使用 keepdims 的地方任意使用 keepdim,会在 Numpy 使用 axis 的地方使用 dim,等等。一个设计特别糟糕的 API 会在 axisdimdimsaxesaxis_idim_i 之间随机切换,而这些都是同一个概念。
  • 尽可能少地引入新概念。 不仅仅是因为额外的数据结构需要更多精力去学习它们的方法和属性,还因为它们会成倍增加理解你的 API 所需要的心智模型的数量。理想情况下,你应该只需要一个通用的心智模型,所有东西都从这个模型中流出(在 Keras 中,就是 Layer/Model)。绝对要避免在你的工作流程中使用超过 2-3 个心智模型。
  • 在你拥有的不同类/函数的数量和这些类/函数的参数化之间取得平衡。 为每个用户操作都设置一个不同的类/函数会造成很高的认知负荷,但参数泛滥也会造成同样的问题——你不会希望在一个类的构造函数中有 35 个关键字参数。可以通过使你的数据结构模块化和可组合来实现这种平衡。
  • 自动化能自动化的东西。 努力减少你的工作流程中所需的用户操作数量。找出用户编写的代码中经常重复的代码块,并提供实用程序来抽象它们。例如,在一个深度学习 API 中,你应该提供自动形状推断,而不是要求用户在他们的所有层中进行心算来计算预期的输入形状。
  • 提供清晰的文档,并附带大量示例。 向用户传达如何解决问题的最佳方法不是谈论解决方案,而是展示解决方案。确保你的 API 中的每个功能都有简洁易懂的代码示例。

我用来判断一个 API 是否设计良好的试金石是: 如果一个新用户在第一天完成了他们的用例的工作流程(按照文档或教程),第二天他们回来解决同一个问题,但在稍微不同的上下文中,他们是否能够在不查阅文档/教程的情况下按照他们的工作流程进行操作?他们是否能够一次性记住他们的工作流程?一个好的 API 是指大多数工作流程的认知负荷都非常低,以至于可以一次性学会。

这个试金石还为你提供了一种量化 API 好坏的方法,即计算普通用户为了掌握一个工作流程需要查阅多少次相关信息。最糟糕的工作流程是那些永远无法完全记住的,每次都需要按照冗长的教程进行操作。


3 - 为你的用户提供有用的反馈。

好的设计是互动的。好的 API 应该可以只依赖最少的文档和教程就能使用——只需尝试那些看起来直观的东西,并根据你从 API 获得的反馈采取行动。特别是

  • 尽早发现用户错误,并预测常见的错误。 尽快进行用户输入验证。积极跟踪人们常犯的错误,并通过简化你的 API、为这些错误添加有针对性的错误信息,或在你的文档中设置一个“常见问题解决方案”页面来解决这些错误。
  • 为用户提供一个可以提问的地方。 否则,你如何才能跟踪你需要修复的现有痛点?
  • 在用户出错时提供详细的反馈信息。 一条好的错误信息应该回答:发生了什么事,在什么情况下发生的?软件期望得到什么?用户如何修复它? 它们应该是上下文相关的、信息丰富的、可操作的。每一条能清晰地为用户提供问题解决方案的错误信息,都意味着少了一张支持请求,乘以用户遇到相同问题的次数。


例如

  • 在 Python 中,以下是一个非常糟糕的错误信息
AssertionError: '1 != 3'

(一般来说,始终使用 ValueError 并避免使用 assert)。

  • 同样糟糕的是
ValueError: 'Invalid target shape (600, 1).'
  • 以下这个例子好一点,但仍然不够,因为它没有告诉用户他们传递了什么,也没有完全说明如何修复它
ValueError: 'categorical_crossentropy requires target.shape[1] == classes'
  • 现在,这里有一个好的例子,它说明了传递了什么,期望得到什么,以及如何修复问题
ValueError: '''You are passing a target array of shape (600, 1) while using as loss `categorical_crossentropy`.
`categorical_crossentropy` expects targets to be binary matrices (1s and 0s) of shape (samples, classes).
If your targets are integer classes, you can convert them to the expected format via:

--
from keras.utils import to_categorical

y_binary = to_categorical(y_int)
--

Alternatively, you can use the loss function `sparse_categorical_crossentropy` instead, which does expect integer targets.'''

好的错误信息可以提高用户的效率和心情。


总结

这些都是相当简单的原则,遵循这些原则可以让你构建出人们喜欢使用的 API。反过来,更多的人会开始使用你的软件,你将在你的领域取得更大的影响力。

永远记住:软件是为人服务的,而不仅仅是为机器服务的。始终以用户为中心。


@fchollet,2017年11月