编写代码很少只是你和你的电脑之间的私事。代码不仅仅是为机器准备的;它还有人类用户。它应该易于阅读、易于被其他开发者使用、维护和构建。当开发者使用他们喜欢的工具,并且保持快乐和高效的状态时,他们就能产出更多更好的代码。不幸的是,开发者经常被他们的工具所拖累,对着晦涩难懂的错误信息咒骂,想知道为什么那个愚蠢的库没有按照他们的想法工作。我们的工具很有可能给我们带来痛苦,尤其是在软件工程这样复杂的领域。
用户体验 (UX) 应该是应用程序编程接口 (API) 设计的核心。一个设计良好的 API,可以让复杂的任务变得简单,与设计精美的床头灯相比,它可能会为这个世界减少更多的痛苦。那么,为什么与家具设计相比,API 用户体验设计常常感觉像是事后诸葛亮呢?为什么开发者中普遍缺乏设计文化?
部分原因仅仅是同理心的距离。当你独自一人坐在电脑前编写代码时,未来的用户只是一个遥远的、抽象的概念。只有当你开始坐在你的用户旁边,看着他们费力地使用你的 API 时,你才会意识到用户体验的重要性。而且,让我们面对现实吧,大多数 API 开发者从来没有这样做过。
另一个问题是我称之为“聪明工程师综合症”。程序员倾向于认为最终用户有足够的背景和上下文——因为他们自己有。但事实上,最终用户对你自己的 API 及其实现方式的了解只是冰山一角。此外,聪明的工程师倾向于把他们构建的东西复杂化,因为他们可以轻松地处理复杂性。如果你不是特别聪明,或者你没有耐心,那么这个事实就会限制你的软件的复杂程度——超过一定程度,你就无法让它工作,所以你只能放弃,从一个更简洁的方法重新开始。但聪明、有耐心的人呢?他们可以处理复杂性,并且他们会构建越来越丑陋的科学怪人,而这些怪人不知何故还能行走。这就会导致最糟糕的 API。
最后一个问题是,一些开发者强迫自己坚持使用对用户不友好的工具,因为他们认为额外的困难是一种荣誉的象征,并且认为设计周到的工具是“给菜鸟用的”。我在深度学习社区中的一些更糟糕的部分看到了很多这种态度,在那里,大多数东西往往是时尚驱动的,而且很肤浅。但最终,这种自虐的姿态是弄巧成拙的。从长远来看,好的设计会胜出,因为它能让其使用者更高效、更有影响力,因此传播速度比对用户不友好的设计要快。好的设计具有感染力。
像大多数事情一样,API 设计并不复杂,它只需要遵循一些基本规则。它们都源于一个基本原则:你应该关心你的用户。 所有用户。不仅仅是聪明的用户,也不仅仅是专家。始终以用户为中心。是的,包括那些困惑不解、经验不足、缺乏耐心的新手用户。每一个设计决策都应该以用户为中心。
以下是我的三条 API 设计规则。
1 - 精心设计端到端的用户工作流程。
大多数 API 开发者关注的是原子方法,而不是整体工作流程。他们让用户在给定的基本原语的基础上,通过不断摸索来找出端到端的工作流程。由此产生的用户体验往往是一长串的黑客攻击,绕过了在单个方法层面上不可见的那些技术限制。
为了避免这种情况,首先要列出你的 API 将会涉及到的最常见的工作流程。大多数人都会关心的用例。你自己去实际体验一下,并做好记录。更好的做法是:观察一个新用户如何完成这些工作流程,并找出痛点。毫不留情地解决这些痛点。特别是
- 你的工作流程应该与用户关心的特定领域的概念紧密对应。 如果你正在设计一个制作汉堡的 API,那么它应该包含一些用户熟悉的对象,比如“肉饼”、“奶酪”、“面包”、“烤架”等等。如果你正在设计一个深度学习 API,那么你的核心数据结构及其方法应该与熟悉该领域的人所使用的概念紧密对应:模型/网络、层、激活函数、优化器、损失函数、时期等等。
- 理想情况下,任何 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 会在表示同一个概念时,随机地在 axis
、dim
、dims
、axes
、axis_i
、dim_i
之间来回切换。
- 尽量少引入新的概念。 不仅仅是因为额外的数据结构需要付出更多的努力来学习它们的方法和属性,还因为它们成倍地增加了理解你的 API 所需的**心智模型**的数量。理想情况下,你应该只需要一个通用的心智模型,所有东西都从它那里流出(在 Keras 中,那就是
Layer
/Model
)。一定要避免在你的工作流程中出现超过 2-3 个心智模型。
- 在不同的类/函数的数量和你对这些类/函数的参数化之间取得平衡。 为每个用户操作都设置一个不同的类/函数会造成很高的认知负荷,但参数过多也会造成同样的问题——你不会希望在一个类的构造函数中有 35 个关键字参数。可以通过使你的数据结构模块化和可组合来实现这种平衡。
- 自动化可以自动化的东西。 努力减少你的工作流程中所需的用户操作数量。找出用户编写的代码中经常重复出现的代码块,并提供实用程序来抽象它们。例如,在一个深度学习 API 中,你应该提供自动化的形状推断,而不是要求用户在他们的所有层中都进行心算来计算预期的输入形状。
- 提供清晰的文档和大量的示例。 向用户传达如何解决问题的最佳方法不是谈论解决方案,而是*展示*解决方案。确保你的 API 中的每个功能都有简洁易懂的代码示例。
我用来判断一个 API 是否设计良好的试金石是: 如果一个新用户在第一天就完成了他们的用例的工作流程(按照文档或教程),并且他们在第二天回来解决一个稍微不同的情况下,他们是否能够*在不查阅文档/教程的情况下*按照他们的工作流程进行操作?他们是否能够一次性记住他们的工作流程?*一个好的 API 的认知负荷非常低,以至于用户可以一次性学会。*
这个试金石还为你提供了一种量化 API 好坏的方法,即计算普通用户为了掌握一个工作流程需要查阅多少次信息。最糟糕的工作流程是那些永远无法完全记住,每次都需要按照冗长的教程进行操作的工作流程。
3 - 为你的用户提供有用的反馈。
好的设计是交互式的。好的 API 应该可以让用户在最少依赖文档和教程的情况下使用——只需尝试那些看起来很直观的东西,并根据你从 API 中得到的反馈采取行动。特别是
- 尽早发现用户错误并预测常见的错误。 尽快对用户输入进行验证。积极跟踪人们犯的常见错误,并通过简化你的 API、为这些错误添加有针对性的错误信息,或者在你的文档中添加一个“常见问题解决方案”页面来解决这些错误。
- 为用户提供一个可以提问的地方。 否则,你如何跟踪你需要修复的现有痛点?
- 在用户出错时提供详细的反馈信息。 一个好的错误信息应该回答:*发生了什么,在什么情况下发生的?软件期望的是什么?用户如何修复它?* 它们应该是上下文相关的、信息丰富的,并且是可操作的。每一个能够清晰地为用户提供问题解决方案的错误信息,就意味着少了一张支持服务单,而且这个数字还会随着用户遇到相同问题的次数而倍增。
例如
- 在 Python 中,以下是一个非常糟糕的错误信息
(一般来说,始终使用 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 月