编写代码很少仅仅是你和你的电脑之间的私事。代码不仅仅是为机器设计的;它还有人类用户。它旨在供人们阅读,供其他开发人员使用、维护和构建。当开发人员感到快乐和高效,使用他们喜欢的工具时,他们会生产出更好、更多的代码。不幸的是,开发人员经常被他们的工具所困扰,对着晦涩难懂的错误信息咒骂,想知道为什么那个愚蠢的库不能按照他们的想法工作。我们的工具很有可能给我们带来痛苦,尤其是在软件工程这样复杂的领域。
用户体验 (UX) 应该是应用程序编程接口 (API) 设计的核心。一个设计良好的 API,可以让复杂的任务变得简单,它在这个世界上所能避免的痛苦,可能比一个设计精妙的床头灯要多得多。那么,为什么与家具设计相比,API 用户体验设计常常让人感觉是事后诸葛亮呢?为什么开发人员中普遍缺乏设计文化?
部分原因是缺乏同理心。当你独自一人在电脑前编写代码时,未来的用户只是一个遥远的念头,一个抽象的概念。只有当你开始坐在你的用户旁边,看着他们在你的 API 中挣扎时,你才会意识到用户体验的重要性。而且,让我们面对现实吧,大多数 API 开发人员从来没有这样做过。
另一个问题是我称之为“聪明工程师综合症”。程序员倾向于认为最终用户有足够的背景和上下文——因为他们自己就有。但事实上,最终用户对你自己的 API 及其实现的了解只是你所知道的一小部分。此外,聪明的工程师倾向于把他们构建的东西复杂化,因为他们可以轻松地处理复杂性。如果你不是特别聪明,或者你没有耐心,那么这个事实就会对你软件的复杂程度设置一个硬性限制——超过一定程度,你就根本无法让它工作,所以你只能放弃,从一个更清晰的方法重新开始。但是聪明、有耐心的人呢?他们可以处理复杂性,他们会构建越来越丑陋的科学怪人,而这些怪人不知何故还能行走。这就会导致最糟糕的 API。
最后一个问题是,一些开发人员强迫自己坚持使用对用户不友好的工具,因为他们认为额外的困难是一种荣誉的象征,并且认为设计周到的工具是“给菜鸟用的”。我在深度学习社区中比较 toxic 的部分看到了很多这种态度,在那里,大多数东西都是由时尚驱动的,而且很肤浅。但最终,这种自虐的姿态是弄巧成拙的。从长远来看,好的设计会胜出,因为它能让它的使用者更高效、更有影响力,因此传播速度比对用户不友好的设计要快。好的设计是会传染的。
像大多数事情一样,API 设计并不复杂,它只需要遵循一些基本规则。它们都源于一个基本原则:**你应该关心你的用户。** 所有的用户。不仅仅是聪明的用户,也不仅仅是专家。始终以用户为中心。是的,包括那些糊里糊涂的、缺乏上下文和耐心的初学者。**每一个设计决策都应该以用户为中心。**
以下是我的三条 API 设计规则。
1 - 精心设计端到端的用户工作流程。
大多数 API 开发人员关注的是原子方法,而不是整体工作流程。他们让用户通过进化式的偶然性来摸索端到端的工作流程,考虑到他们提供的基本原语。由此产生的用户体验往往是一长串的 hack,绕过了在单个方法层面不可见的 technical constraints。
为了避免这种情况,首先要列出你的 API 将会涉及到的最常见的工作流程。大多数人都会关心的用例。自己实际去体验一下,并做好笔记。更好的做法是:观察一个新用户如何完成这些工作流程,并找出痛点。毫不留情地解决这些痛点。特别是
- **你的工作流程应该与用户关心的特定领域的概念紧密对应。** 如果你正在设计一个制作汉堡的 API,那么它应该包含一些毫不奇怪的对象,比如“肉饼”、“奶酪”、“面包”、“烤架”等等。如果你正在设计一个深度学习 API,那么你的核心数据结构及其方法应该与熟悉该领域的人所使用的概念紧密对应:模型/网络、层、激活函数、优化器、损失函数、epochs 等等。
- **理想情况下,任何 API 元素都不应该涉及实现细节。** 你不希望普通用户去处理“primary_frame_fn”、“defaultGradeLevel”、“graph_hook”、“shardedVariableFactory”或“hash_scope”,因为这些都不是底层问题领域的 concept,它们是你内部实现选择的 highly specific 的概念。
- **精心设计用户入门流程。** 完全的新手如何才能找到用你的工具解决他们的用例的最佳方法?准备好答案。确保你的入门材料与你的用户关心的事情紧密相连:*不要教新手你的 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` 之间随机切换。
- **尽可能少地引入新概念。** 这不仅仅是因为额外的数据结构需要付出更多的努力才能了解它们的 methods and properties,而且是因为它们会成倍增加理解你的 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 月