Skip to content

8. 交互设计

让每一个细节都做到完美,并把需要做到完美的细节数量控制到最少。

— Block 创始人 Jack Dorsey

可供性指的是产品可以被使用的方式,不论是不是设计者原本打算的。沙发的可供性是“可坐”,但猫会发现它还有“可抓挠”的可供性。两者都算可供性,尽管说明书里通常只会写其中一个。

可供性也可能有危险。你去过露天演唱会吗?有没有注意到椅子常常被紧紧绑在一起?这种绑法是在防什么“非预期可供性”?它通过防止椅子挡住人群逃生通道、以及被人抡起来投掷,来提升现场安全性。

由于代码和协议不够安全,数字世界里充斥着各种不受欢迎的可供性:垃圾短信和邮件、隐私泄露、洗钱、钓鱼攻击等等。有些问题确实难以避免,但也有不少本可通过更有前瞻性的设计来规避。

Don Norman在其经典著作 The Design of Everyday Things 中普及了“可供性”这一概念,并提出了 意符(signifier),这一概念也构成了第 2 章的讨论框架。如果可供性是产品能做什么,那么意符就是我们判断产品该怎么用的线索。

椅子虽然有“可投掷”的可供性,但它通常没有明显意符来提示这件事。那些希望表达“可投掷”的物品,往往会用握把、可单手抓握的球形外观,或空气动力学造型来传达这一点。

要看清意符和可供性的区别,我们不妨看看类的公共接口。在 Java、C# 这类面向对象语言里,public 方法是可供性,而 public 关键字就是一个意符,告诉类外调用者这些方法可用。private 则是一个只面向类内调用者的意符。

在 Python 里,def 是方法的意符。由于 Python 没有真正的私有方法,开发者通常约定在名称前加 _,表示“不打算给外部调用”,但唯一约束其实是“君子协定”。从某种意义上说,它是在把方法对外隐藏。我甚至觉得它像一种反意符,类似“KEEP OUT(禁止入内)”标志。

从类外调用者的视角看,方法可以按表 8-1分类。

表 8-1. 方法可供性与其是否被标记为可供外部调用
类型可供性?有意符标记?说明
public method (Java/C#)yesyes用户预期可以调用,也确实可调用。
private method (Java/C#)nono不能调用,用户也不会预期它能调用。
normal method (Python)yesyes
underscored method (Python)yesno技术上可调用,但前导下划线在提醒人们不要调用。
publicly accessible method that fails because it’s not implementednoyes看起来提供了,实际上不可用。

意符和可供性通常相伴出现。在在线单选题里,单选按钮意味着最多只能选一项;而一组复选框意味着可以多选。不同语义会对应标准化的视觉模式。

当你设计接口时,请先枚举其可供性,并逐一思考它们的正当用法与误用方式,去掉那些不受欢迎的可供性。这需要大量设计品味和迭代:例如,允许高吞吐场景在什么边界上会变成“有人故意刷请求把你服务打挂”的拒绝服务攻击?

对于那些你确实要暴露的可供性,要用意符把用户引导到最安全、最有价值的路径上。

Tip

软件接口设计在很大程度上就是:决定暴露哪些可供性,以及如何向用户传达这些可供性。

本章我会讨论我们在“暴露什么功能、如何暴露”时所做的设计选择。结合可供性、意符、场景与人物画像,你将能够:

  • 强化产品的正确用法,弱化错误用法。
  • 更明智地把握可供性的发布时机。
  • 理解何时应该让功能可扩展,何时应该只解决特定问题。

以我的经验看,大多数设计争论都围绕这些取舍展开,而我们都值得把这些问题想得更清楚。用户场景和人物画像为此提供了具体可操作的方法。

在进入具体设计建议前,我先搭一层脚手架:

  • 偏见与意识形态在软件设计中的作用
  • 好设计的前提条件
  • 一个用于承载建议的案例研究

软件设计中偏见与意识形态的作用

软件工程师会把自己的意识形态信念和偏见带进工作里。

例如:

  • 有些人偏好设计灵活、对高阶用户友好的产品;另一些人偏好强主张、安全、易用的产品。
  • 有些人偏悲观,更关注功能和产品的负面影响;另一些人更关注优势与机会。
  • 有些人认为自由/开源软件在透明性、伦理和安全上更优;而专有软件的支持者则强调知识产权和商业激励对创新的重要性。

意识形态可以带来力量感和使命感,但在软件设计中,它们只能是起点。最优产品始终应该更多由用户及其需求来塑造,而不是由设计者的信念来决定。

下面我会快速看这三个例子在实践中的表现。

争论:灵活还是强主张

看看现代智能手机行业。苹果 iOS 和谷歌 Android 的设计者起初意识形态差异很大,但产品逐渐趋同。苹果开放了更多灵活的开发者平台;而谷歌及其硬件伙伴则收紧了某些自由度,提供更多强主张体验。

这些变化既有商业和监管原因,也有产品设计原因。这种“趋同进化”说明两家公司都在倾听相似用户和合作伙伴,他们面对的是相似场景。

争论:乐观还是悲观

每一波新技术都会引发一波乐观情绪,也会引发对其社会影响的道德恐慌。上一个十年是社交媒体,如今是 AI。更好的软件设计往往是解决问题的重要组成部分。

举个例子,人们担心 LLM 聊天机器人会削弱人的批判性思维能力,也担心它会让犯罪变得更容易。

截至本文写作时,这件事还处在早期阶段,但我们已经看到:

  • Red teaming:研究人员构造对抗性场景,再让 AI 在这些场景下接受“压力测试”,以增强其对生物武器、身份盗窃等威胁知识泄露的防护能力。
  • Tutoring modes:LLM 提供方正在开发学习辅导模式,服务学生场景,目标是帮助学生形成批判性思考,而不是直接把答案喂给他们。

我猜随着时间推移,我们还会看到面向不同场景和人物画像的家长控制及其他保护机制。

争论:开源还是专有

在 2000 年代,大多数产品要么完全专有,要么完全开源。很多现代软件公司采用的是介于两者之间的模式:

  • 在 open core 模式中,大部分功能开源,但某些企业用户场景由专有功能支持。
  • 在双重许可模式中,许可条款按人物画像划分,例如对商业用途增加限制。

这些模式试图在创新激励与开放透明之间取得平衡。

如果我们发现自己只是在用意识形态争论,就该反思:我们是否真的足够了解客户及其使用场景。你在本书前面学到的许多技术,都能帮助你做出更好的设计决策。

好设计的前提条件

本章不少设计建议都依赖于场景、人物画像,在某些情况下还依赖迭代开发。这些前提并不保证产出好设计,但它们确实很有帮助。我们将充分利用本书前面建立的这些基础:

  • 在开发实践中建立收集反馈并快速迭代的能力。这样一来,即便某个接口最初过于受限,你也有信心后续补强。这一点在第 5 章已详细讨论。
  • 形成对你的目标受众及其诉求的清晰画像,我在第 6 章讲过。举例来说,如果他们都想要同一件事,就把它做成唯一选项;如果诉求不同,就在恰当位置提供灵活点。
  • 场景驱动的发现方法(第 7 章)去推敲客户将如何使用你的产品。如果这些场景揭示出安全缺口,看看能否在不牺牲通用性的前提下把缺口补上。

有了这些工具与方法,你就不必频繁诉诸意识形态捷径,也能在信息最充分、动机最充足的时候做出设计决策并落地实现。

基础打好之后,来看一个案例研究,帮助我们穿过具体设计原则。

案例研究导入

在 2010 年代早期,我负责过 Facebook 社交网络图数据库对象建模的 schema 设计。对象(称为 entities)可能是用户、帖子、群组、活动等;edges 则是连接关系,比如好友关系、成员关系、帖子作者等。这个 schema 叫作 EntSchema,即 “entity schema” 的缩写。

我们其中一个北极星场景是:

  • Schema authoring:模型创建者只写一份表示,就能自动产出他们需要的一切:数据库 schema、读类、写类。

如果这就是唯一的北极星场景,最基础版本大概是这样:

class PostSchema(EntSchema):
    def db_config() -> DatabaseConfig:
        return (DatabaseConfig()
            .name("posts"))

    def fields(self) -> dict:
        return {
            "id": int64_field().primary_key(),
            "text": varchar_field(4096),
            "created": int64_field(),
            # ...
        }

    def edges(self) -> dict:
        return {
            # An author can have many Posts.
            "author": edge(UserSchema, cardinality=EdgeCardinality.ManyToOne),
            # ...
        }

这会生成:

  • 一个数据库表,schema 包含 idtextcreated
  • 一个把作者连接到其帖子上的边表
  • 一个用于 Python 读取的类 Post
  • 另一个用于 Python 创建/编辑帖子的类 PostMutator

下面是 Post

class Post:
    @property
    def text(self) -> str: ...

    @property
    def created(self) -> str: ...

    @property
    def id(self) -> int: ...

    async def fetch_author(self) -> User: ...

注意,这个 schema 里几乎只包含数据库层直接有用的信息。这正是满足该场景所需的最低配置。

当时我引入 EntSchema 之前,数据库和代码库里已经有大量手写实体。若保持这种简单的数据库层表示,一个巨大优势是:我们可以根据已存储的数据库表示自动生成 EntSchema,因为 schema 里除了数据库已有信息外不再额外要求别的内容。这样一段简单脚本,就能在超大代码库迁移中给我们极大助力。

不过,schema 作者并不是唯一人物画像。我们还得考虑“阅读和使用 schema 的人”。例如工程师常用对象检查器排查线上问题。

假设我们要求 Post 作者提供更丰富的类型信息,并把 created 字段声明为时间戳而不只是整数,那么用户在调试 Post 对象时就能看到可读性更好的格式化时间。

但这会不会只是范围蔓延(scope creep)?增量策略也有风险:如果我们先做容易的、只含底层信息的版本,后面再补充丰富类型信息会很难改造。

这个案例会展开我们如何决定“作者应向 EntSchema 额外提供哪些信息”,以及背后原因。到本章结束时,我们会把前面展示的 PostSchema 演进成能够覆盖更多场景的版本。

Go 生态里的 Ent

有个流行的开源 Go 框架叫 ent,基于 EntSchema。你可以看看它,先建立整体感受。不过它在字段类型系统上的丰富度不如 Meta 内部版本。

把注意力引向正确用法,远离错误用法

一个设计良好的产品,会用意符和默认配置把用户引到高产且安全的可供性上,同时确保用户不会误入风险更高的路径。

可供性可以像交通灯一样,被传达为“更欢迎”或“没那么欢迎”。绿色可供性应该安全且通常有效;黄色表示谨慎;红色表示禁止。

Tip

根据推荐程度把可供性分为绿、黄、红三色,并据此设计它们的意符。

为了说明这一点,想象你点击一个可能可疑的网址链接:

  • Green:链接跳转到同一网站内的另一页面。
  • Yellow:链接把用户带到站外页面。系统可能弹出“你确定要前往这个外部网站吗?”之类警告,避免用户误去冒充源站的钓鱼站点。
  • Red:用户点击的链接指向一个可疑网站,且该网站缺少有效安全证书。浏览器会设置很高门槛才允许继续访问。

绿、黄、红可供性都可以存在,但你也可以彻底移除某些可供性。比如用户点击进入的页面已知恶意,浏览器就可以完全阻止访问。

一个让绿色场景高度可发现、而让黄色或红色场景不那么显眼的产品,用起来会非常愉悦。用户可以更有把握地推进操作,逐步建立足够信任,不用步步自我怀疑。

下面是一些把用户引导到最有价值可供性的常见技巧1

  • 选择安全、可预测的默认值。
  • 优化“阻力最小路径”。
  • 在正确场景下把可供性给到正确人物画像。
  • 做好校验。

下面我逐一展开。

选择安全、可预测的默认值,或者干脆不设默认值

灵活性和易用性结合起来:给大多数人提供合适默认值,同时允许更挑剔的用户自定义。不愿深入设置细节的人应获得合理行为;愿意投入时间定制的人则可以得到更强能力。

用户会因为你能选出好默认配置而信任你,也愿意为此付费。

为方便起见,EntSchema 默认假设数据库列名与用户侧声明名称一致,但也可定制。比如 PostSchema 维护者觉得 “posted_at” 比 “created” 更清晰,于是想改名。他可以这样写:

"posted_at": integer_field().db_config(name="created"),

这样既保留灵活性,又不必让每位 schema 作者都重复写数据库列名,也不必为了改代码侧名字而发起数据库迁移。

默认值不仅是便利。错误默认值的后果可能涉及政治争议,甚至灾难:

  • Microsoft、Apple、Google 等操作系统公司都曾因把特定浏览器或搜索引擎设为默认而遭遇诉讼。
  • 早期 Internet Explorer 默认启用 ActiveX 控件,造成严重安全漏洞,使多种病毒和蠕虫传播。
  • 2009 年,Facebook 把用户新帖子默认可见范围设为 “Everyone”,引发强烈反弹并导致用户信任受损,即便后来提供了更明确控制也长期难以恢复。

Facebook 这个例子引出一个最容易被忽略的默认值:不设默认值。开发者习惯给所有参数都配默认值,让调用点更简洁、产品表面更丝滑,但这可能是伪命题式取舍。若两个选项任一都可能不安全、不符合预期或无效,就应让用户做有意识决策。自动驾驶汽车不会默认左转或右转,而是要求导航者输入目的地来辅助决策。社交媒体也应提示新用户对受众范围做知情选择。

Tip

产品接口质量可以用它向用户提出的问题是否“相关且必要”来衡量。

看个实际例子。PostSchema 有个重大的数据库问题:数据库有很多分片,而常见查询是“获取某作者最近所有帖子”。若这些帖子不在同一分片,代价会非常高,因为要扇出到所有分片去查。

如果帖子都在同一分片,查询性能就会很好。因此,PostSchema 的作者应在数据库配置里声明共置关系:

def db_config() -> DatabaseConfig:
    return (DatabaseConfig()
        .name("posts")
        .colocateWith("author"))

如果 EntSchema 在未声明时默认“无共置”,帖子就会随机散在各分片。创建者可能直到上生产、甚至直到规模上来后才意识到这个错误。

因此这里不该有默认值。必须强制用户做选择,而我们要尽力把取舍讲清楚。

接下来我把这个“选好默认值”的准则进一步泛化。

优化你的阻力最小路径

用户在使用产品时并不总是深思熟虑。他们常会自然走向阻力最小的那条路。

失灵交通灯(malfunctioning traffic light) 指的是两个功能在“相对可发现性或相对易用性”上发生了本不该有的倒置。你可以想象交通灯被倒过来了:本应在上方的绿色可供性,却跑到了黄色甚至红色可供性下面。

例如,设计者本应默认一个绿色可供性(在 Facebook 仅与好友分享),却默认了黄色可供性(公开发帖),这就是失灵交通灯。

这里更关键的是不同可供性之间相对的易用性和意符强度,而不是产品绝对易用性或绝对可发现性。这种情况非常常见。

我们设计这些应用层 EntSchema 字段类型时,做了字符串子类型,比如邮箱、自然语言文本、枚举、电话号码。开发者可写成 email_string_field 之类。

但我们有个问题:我知道不可能预先覆盖所有字符串类型。用户以后可以新增,但这似乎太麻烦,于是我加了一个兜底 string_field

结果很糟:最没帮助的选项反而最容易被发现!许多 schema 作者开始过度使用 string_field。它的意符太“顺手”了,看起来和他们在其他系统里预期的一样。我猜很多工程师甚至没注意到还有别的选项。也有人可能知道,但打算回头再处理,后来就忘了。

也就是说,黄色可供性 string_field 比绿色可供性 email_string_field 更容易被发现。就像我们的交通灯把黄色放到了绿色之下。

string_field 也更省事,更短,输入成本更低,不需要额外思考;而其他类型需要开发者浏览字符串类型菜单并做审慎选择。

绿色可供性 enum_string_field(MyEnum) 比黄色可供性 string_field 更难用。再次说明,我们的交通灯失灵了。

每条声明单看都不复杂,但由于相对可用性和可发现性失衡,系统整体效果就不好。

为修复这一点,我们移除了 string_field,换成更直白的 custom_string_field。这让它和其他字符串子类型处在同一认知层级,也向用户传递出它并非主流选项的信号。

这解决了可发现性倒置,但忙碌的开发者可能仍会滥用它。比如从别处复制粘贴进来,或先当占位,之后忘记回填。

所以我又加了一点摩擦:强制他们加校验器。比如 schema 作者若想接受正则表达式输入,可以写:

'my_regex_field': custom_string_field().validator(lambda x: is_regex(x))

他们当然总能塞一个空校验器,但我觉得这样至少足以推动大多数工程师认真想一想。即便没做到,我也给代码评审者增加了一个意符,他们能注意到缺少校验器并指出问题。

custom_string_field 远没有 string_field 那么诱人,也显著提升了 schema 声明的准确性。

在正确场景下把可供性给到正确人物画像

请认真思考“把选择权给谁、何时给”。

在设置 Post 的三个字段 idposted_attext 时,对于创建帖子的开发者来说,可能出什么问题?

post = await (PostMutator()
    .set_id(rand())
    .set_posted_at(now())
    .set_text(some_user_input)
    .create())

几个常见坑:

  • ID 应由数据库从其 ID 空间分配,以避免重复并保证分片均衡,不该让用户手动给。
  • id 可能被不小心漏掉。
  • posted_at 可能漏填,或被设为 0。(如果你见过 1969 年 12 月 31 日这种时间戳,很可能就是这类 bug。)
  • posted_at 也可能被误设为其他显然无效的整数。

schema 作者如何构建一个“成功之坑”,让创建和编辑对象的人不容易踩雷?

其实不需要太多额外工作,schema 作者就能在字段声明中注入更多语义,把这些场景全部规避:

def fields(self) -> dict:
    return {
        "text": natural_language_string_field()
            .db_config(type='varchar(4096)'),
        "posted_at": timestamp_field().at_creation(),
        # ...
    }

我们做了几个改动,通过减少工程师需要做的决定,扩大了成功之坑:

  • 彻底去掉 id 字段声明,统一在创建时自动生成。
  • posted_at 声明为时间戳,可做范围校验;同时标记为特殊的 at_creation 时间戳,这样 PostMutator 会自动填充,无需用户输入。

更精确地说:

  • 我们把 id 的决策权,从“不太清楚底层细节”的应用开发者,转移到“清楚底层机制”的数据库工程师。
  • 我们把 posted_at 的决策权,从创建 Post 的人,转移到编写 PostSchema 的人。

Tip

仔细判断:哪些选择应该由哪类人物画像来做。

多人物画像的引导流程

面向 SaaS 的采购流程往往涉及多种人物画像。如果你的产品面向一线执行者(IC)使用、但销售对象是公司,那么你可以想象:IC 对采用产品很兴奋,却在流程中突然遇到付款页面。他们通常拿不到公司支付权限,而有权限的人也未必愿意把公司信用卡信息通过聊天工具发给他们。

相比之下,让 IC 先提供采购负责人的邮箱通常更容易。IC 可以提前通知对方留意邮件,然后由财务人员点击链接完成表单。

除了找对人物画像,我们还应在正确时机提供可供性,也就是用户既具备决策所需信息、又有动力做决策的时候。

前面提到 EntSchema 的共置(colocation)。那 schema 作者应该何时决定数据库布局?是不是一开始就得想清楚?

开发时我们始终关注的一个关键场景,是所谓“黑客松场景”:用户在做快速原型,希望尽快勾勒产品模型、接上 UI,并演示一个产品想法。

为此我们做了一个面向原型用户的草稿模式。用户准备走向生产时再退出该模式。在此之前,许多生产就绪检查都会关闭,包括“必须选择共置关系”的检查。这样既把决策延后到用户更清楚自己要什么的时候,也把额外工作延后到作者确定要生产化的时候。

“正确的人”在“正确的时间”做决策后,下一步就是校验这个决策。

执行校验

要对用户决策做校验。我们给 text 字段加点校验

"text": natural_language_string_field()
    .max_length(4096)
    .user_input(),

首先,我把 varchar(4096) 这种数据库层声明上提为 schema 中的 max_length。这样我就能在 API 层更早校验输入长度,而不是等到数据库层才失败。

正如第 3 章所讨论,我这样做实现了:

  • 左移校验,更早发现问题,减轻数据库负载
  • 在接口层校验,从而返回更清晰的应用层错误信息

其次,我把该字段标记为 user_input,而不是“由内部工程师选择的值”。这意味着我们可以统一做恶意 URL 和其他安全漏洞筛查,而不必依赖每个创建帖子的调用点各自处理。

既然我们已经讨论了功能设计和“成功之坑”的构建技术,接下来聊聊:功能到底要不要加。

明智把握可供性的发布时机

人们常说技术可为善也可为恶,而最安全的可供性往往是“它不存在”。即便看似无害的功能,比如让社交网络用户自定义姓名,也会打开脏话和滥用等攻击面,所以我们必须把大多数对外暴露点都加固。

因此,暴露一个没想透的功能,本质上是在下注。

在理想世界里,如果我们持续迭代并倾听用户,那么对候选功能的问题就不是“该不该加”,而是“什么时候加”。如果你暂时不做某个重要功能,之后补上就行。

在这种环境下,对暴露面保持一点保守是划算的。等你拿到更多用户细节需求,再加也不迟。

Tip

拿不准,就先不做。

如果你过于激进、过早发布功能,可能出现很多坏事

  • 你缺少足够用户数据,只能依赖意识形态默认,导致设计决策失真。
  • 测试不足,产出粗糙。
  • 用户难以上手,因为你没留出足够设计打磨时间。
  • 你本可以把时间投入到更有价值的东西。
  • 该功能可能带来你尚未准备好的维护成本和长期所有权压力。
  • 若你构建的是需稳定性的 API,后续修订与兼容性破坏会让用户不满。

注意:当功能不是团队最高优先级时,你更容易在这些地方偷工减料。所以有个推论:

Tip

只有在你能投入足够专注度把事情做好时,才去做该功能。

“拿不准就先不做”的另一个推论是:不要一直拿不准。形成“是否需要该功能”的信念,可能只需和用户做两次访谈(我在第 6 章介绍过)或看一眼指标。要么你据此把功能做出来,要么你确认它没你想的重要。两种结果都很好。我们工程师都喜欢“自己有点子”,但管理者更喜欢我们验证假设、用数据学习,用户也更喜欢由此带来的结果。

让我从理想的迭代世界退一步,考虑现实。现实里发布功能有各种固定开销:代码评审、部署、知识传递、管理、文档等。因此我们有时会出于务实,把一些低优先级“锦上添花”与关键功能打包发布。但我们的元目标应是降低发布开销,从而最小化这类权衡,让团队目标与用户目标更一致。

以下是一些在理论和实践中都好用的发布准则:

  • 值得做,就值得验证。
  • 不做盲目乐观者,也不做盲目悲观者。
  • 应用“三三法则”。
  • 分阶段构建。
  • 必要时,先上实验版。

值得做,就值得验证

不要发布未经验证的功能。很多工程师喜欢“顺手”塞进一些额外可供性,以防用户需要。

这没问题,但如果你连写一个测试的时间都挤不出来,这通常是坏信号。

第 4 章中,我介绍了多种判断功能是否有效的方法。写测试应该是最低标准。

不要只测 happy path。还要找出你暴露出的负向可供性并覆盖测试。例如用户传入坏数据怎么办?出现竞态条件怎么办?

既不要乐观主义,也不要悲观主义

另一个意识形态默认是乐观与悲观之争。有些工程师天生乐观,构建时只看 happy path;另一些工程师更悲观,直觉上会先测试想法、寻找负面影响,对复杂度持怀疑态度。

这些心态都可能是优势来源。例如悲观者可能成为优秀安全工程师,乐观者可能在尚未意识到挑战规模前就敢于创业。

但在软件设计里,它们也可能成为弱点。我们如何超越先天气质,成为产品真正需要的“平衡型设计者”?场景可以帮助我们建立对设计的信心。

每个功能都有代价,从界面变杂乱到直接伤害用户不等。设计者应构造对抗场景来“破坏”自己的想法。在安全领域,这在设计阶段叫 threat modeling,在代码写完后叫 red teaming

反过来,替用户攻克难题正是产品创造价值的方式之一。太容易的东西,别人也容易复制。要主动头脑风暴你功能可能带来的正向场景。

同时考虑乐观和悲观视角,我们就更可能找到“放大绿色可供性、抑制红色可供性”的方案。

无论你个人偏好如何,都要同时模拟绿色与红色场景。你甚至可以和一个倾向与你相反的工程师结对,也许就是那个你在这些问题上经常争论的人。

应用“三三法则”

在消费软件里,构建功能时你经常会收到来自高阶用户或朋友的“奇特需求”。企业软件也类似。有时某个付了很多钱的企业会施压,要求你满足其独特需求。

不能什么都答应。你无法高质量维护成百上千个功能,客户也不想要臃肿、迷宫式界面。甚至提出需求的客户自己也不想成为“唯一使用者”,因为那意味着他们在你的测试矩阵里会沦为边角料。

但如果你从不同客户那里收到三条反馈,并且都能由同一个功能解决,那么很可能还有更多用户会逐步受益。它大概率值得做。

前提是你得足够理解用户背后的场景,才能做出这个判断。

你也可以尝试用三个用户模拟来论证某个功能。比如在 EntSchema 中,我们曾经要决定是否把描述性注释做成 schema 的正式组成部分,像这样:

def fields(self) -> dict:
    return {
        "text": natural_language_string_field()
            .description("What the user wrote, in plain text"),
        "posted_at": timestamp_field().at_object_creation()
            .description(
                "When the user posted this.  If they put it on a schedule, "+
                "this is when the post was scheduled to go live."),
        # ...
    }

这会增加 schema 作者工作量;我们本来也可以让他们仅在 schema 定义里字段旁写行内注释。但把描述做成正式字段有多个场景收益:

  • 注释可进入代码生成的读取接口,让用户在 IDE 里跳转定义时看到说明。
  • 写入接口也同理。
  • 我们计划将来生成网页文档(后来也确实做了)。
  • 线上对象检查器可以把描述内联展示或做成 tooltip,帮助用户理解和调试对象。
  • 我们还能校验大家是否真的写了描述,把覆盖率做得比普通注释更高。稍后我会再讲这个点。

它高分通过了“三三法则”,所以我们做了。

Tip

如果你能为某个功能想到三个有说服力的场景,或者有三个用户都提出该需求,就应认真考虑去做。

有些功能不可能一次做完,因此你需要规划分阶段发布。

分阶段构建

有时,你想支持某个用户场景,但它并非最高优先级;可如果现在什么都不做,后续再补会难很多。你面对的是一种“活板门决策(trapdoor decision)”,即一旦做出选择,之后很难回退或补救。

还有些时候,功能太复杂,无法一次发布。你需要先上早期版本、收集反馈,再完成后续部分。

这时,构建过程中应先设一个边界(perimeter)。可以想象施工现场外的铁丝网:它把人挡在外面,工人才能在里面安全建房。你最终会邀请用户进来,但得等墙体、设施和安全要素到位之后。只要普通用户还在边界外,你就对“做什么、怎么打磨”保有很大调整空间。

这样的边界能为你争取时间,后续再把功能补全。

以下是一些“先设边界、再分步推进”的例子:

  • 你想给服务提供免费层,但前提是先做好反垃圾与效率优化。那在此之前,边界就是付费墙。
  • 你想给出一个好默认值,但暂时不确定该怎么选,或还需要额外打磨才能适配所有人。那就先强制用户做知情选择;等你知道用户偏好、或打磨就绪后,再取消强制。
  • 某未来功能需要采集数据。若不提前采,后面很难为存量用户补采。那你就先要求用户提供这些数据,即便这可能劝退一部分早期用户。后续若影响增长或引发抱怨,再取消该要求。

EntSchema 的描述功能属于最后一类。我们最高优先级是提升新模型编写速度,但我们也知道“模型使用者的优质描述”同样重要。若不前置要求描述,我们几乎可以确定大多数作者不会写,后续就得做迁移补齐。这个功能加起来很快,所以我们先做了。

但我们也担心额外摩擦过多会劝退 schema 作者。理论上可能存在“两全其美”平衡点,但我们当时还有更关键问题要处理。

我们的边界策略是:起步阶段先禁止无描述字段。

根据反馈,我们很快又加了 .self_explanatory() 标签,供想跳过描述的人在字段和边上使用。可这属于黄色可供性,因为它很容易被滥用于跳过关键文档。所以我们又要求每个 schema 至少有一部分字段必须写描述,强制用户至少做一遍稀疏注释。(为避免打扰原型用户,我们在草稿模式下不做这项检查。)

后来我发现大量无意义描述,例如给 Posttext 字段写“the text of the post”。于是某次黑客松里,我加了启发式校验去标记这类空话注释。若整段文本只包含类名、字段名和 “of”“the” 之类填充词,我就判它无效,要求补充细节或改用 self_explanatory。(我担心这会让人烦,可能确实有点,但也收到过几条带笑脸的“被你抓到了!”留言,很多工程师其实乐于被这样轻推一把去写得更认真。)

我们先用简单但明确的方式起步,之后再根据反馈打磨功能、逐步下调边界,最终在易用性和抑制黄色可供性之间取得平衡。如果在完全没有用户反馈、而我们还在摸索大量基础问题时就试图一次做到这么细,那反而为时过早。

必要时先上实验版

在 Facebook,我们有句话:“code wins arguments(代码胜过争论)”。意思是,可运行、可演示的代码,比抽象讨论、场景推演、需求文档等更有说服力。

发布功能时,你不可能永远做到信息完备、信心满格。有时最好的调研方式就是先做出来,再验证假设。

在这种情况下,你可以通过内部试用功能(第 4 章)或把它作为实验发布(第 5 章)来获取更多信息。用功能开关向小比例用户灰度,甚至做 A/B 测试;发起 RFC 征求意见;找 beta 用户参与测试。

最后你可能会把功能下线,但这也没关系。

就 EntSchema 来说,我们没有做线上实验的条件,但通过内部试用完成了验证,并学到了原本不知道的问题。我们先自己写了前几个 schema,在用户上手前先体验其痛点和爽点;也把一些经过战火考验的旧模型迁移到 EntSchema,确保覆盖真实生产场景而非玩具案例。用真实使用来检验设计,帮助我们更早确定优先级最高的功能。

当我们决定了“功能是否发布、何时发布”之后,还要决定“以多泛化的形式暴露”。

窄功能 vs 可扩展功能

我们该做可扩展版本以覆盖更多用例,还是做面向特定场景的窄版本?该如何决策?

你可能有个人偏好,倾向灵活接口或强主张接口。基于场景的设计能帮助你跳出这些默认倾向。

当我们决定是用数据库层类型(如 varchar_fieldint64_field),还是用应用层类型(如 timestamp_fieldstring_enum_fieldemail_string_field)时,我们找到了三个很有说服力的场景动机:

  • 输入校验:对象创建者希望在写入损坏数据前就确保枚举值有效且已填写。邮箱等字段也应满足各自格式要求。
  • 可读性:用户希望只看字段和边的类型就能理解它们用途。
  • 生产环境对象检查器:工程师可从 timestamp_field 看到格式化时间,把 url_string_field 里的网址变成可点击链接,或从 ID 字段直接渲染到 User 对象的链接等。

综合来看,这些用例之所以打动我,是因为它们触发了另一版“三三法则”。

Note

如果一个可扩展版本能在短期内解锁三个彼此不同、且有力的场景,就做它。否则,先做用户当前最关心的一两个场景所需的定向版本。

在评估“可扩展版本”的构建成本时,还要记住几点:

  • 值得发布,就值得测试。若你说覆盖三个场景,就为每个场景写一个场景测试(第 4 章),确保扩展性不是幻觉。
  • 扩展性会增加复杂度,通常也会带来更多红色和黄色可供性。要同步发现并缓解这些风险。
  • 警惕不同用例优先级差异过大。以上列表里,优先级其实是递减的,其中运行时校验最紧急。我们原本可以只做一个更具体的方案,比如只做校验器。但最后那个场景也很有价值,所以我们选择先打基础、把对象美化展示推迟到有时间再做。换言之,我们前期先铺路,不急着一次性全做完。

再看一个“过早扩展”的例子。假设你在一家工作室做第一款游戏。工作室梦想做很多款游戏,于是你很想顺手搭一个通用游戏平台。但如果你连第一款游戏能否顺利做完都还不确定,也许更好的策略是先把通用性压到最低。等你有资金做第二、第三款时再抽象。额外好处是,到那时你会更清楚不同游戏间哪些是共性、哪些是差异,从而选出更合理的抽象。

章节小结

EntSchema受益于我们对场景方法的应用,从而提供了安全且高效的开发者体验。而“三三法则”也确实有效:EntSchema 最终演化成了通用开发平台,动力来自它从 schema 作者那里沉淀下来的知识。多年下来,它支撑了大量能力,例如网页文档、对象检查器、数据迁移工具、数据完整性检查、GraphQL schema 生成等等。它起初只是提升开发效率的工具,后来因为其中一些能力具备关键任务属性,最终变成所有实体都必须使用的基础设施。

本章核心是打造可用且安全的接口。这里回顾几条建议:

  • 不要教条化。深入人物画像与场景细节,再决定做什么。
  • 关注意符设计,确保它突出最佳可供性、弱化不良可供性,帮助用户落入“成功之坑”。
  • 你交给用户做的每个决策都应当重要、可校验,并由正确人物画像来承担。
  • 既不要盲目乐观也不要盲目悲观。用场景同时放大功能收益与潜在副作用。

下面这些建议,则把“时间与迭代”加入了场景/人物画像之外的思考维度:

  • 你在专注时工作质量最好,所以要在功能真正高优先级、值得专注时再投入建设。
  • 当你需要规避活板门决策时,先设防护边界,通过数据采集与访问限制来保留未来选择空间。
  • 与此同时,别忘了三三法则:不要在缺乏多个有力场景支撑的功能或扩展点上投入过多时间。
  • 最后,如果你仍无法决策:拿不准,就先不做。希望你能在条件更清晰时再补上。

希望你会喜欢并实际运用这些技巧。它们只是经验法则,但在我看来,用这些框架去思考艰难产品取舍,能让自己和团队都更清晰、更笃定。

练习

让我们一起做一个密码管理器。(想想 LastPass、1Password 或 Bitwarden。)你的目标人物画像是数字原生用户:他们希望为所有应用和网站安全保存高质量密码,而且不用全记在脑子里。当然,他们也希望创建、修改、存储密码都足够易用,同时尽量不易被误用。他们愿意每月支付一小笔费用,换取“再也不用操心密码”。

这组练习我选一个大家都熟悉的话题,所以先聚焦传统密码。Passkeys 虽然更现代也更安全,但目前普及度还没那么高,这里先放一边。

  1. 先确保用户具备良好密码卫生习惯。请列出在我们的管理器里“创建密码”时的绿色、黄色、红色可供性。考虑两个大场景:第一,创建新账号凭据;第二,录入他们过去已创建账号的登录信息。
  2. 参考第一问答案,至少提出两种设计思路,让密码管理器更突出绿色可供性,同时压低黄色与红色可供性。可聚焦“用户在标准注册网页上创建账号”这个场景思考。
  3. 你在考虑建设一个网站域名数据库,并维护这些域名的最新安全信息以保护用户。这是个大投入,因为你需要建立从安全社区和用户侧摄取数据的机制,因此必须有充分理由。假设唯一备选方案是依赖第三方数据库(仅提供疑似钓鱼域名列表)。结合“三三法则”,试着判断是否存在足够多且足够重要的场景,支持你自建一个更通用的数据库。

答案

  1. 下面给出一些可供性及对应场景,用来说明推荐程度。
    • Green:用户选择一个强随机、自动生成、且对特定应用唯一的密码。
    • Yellow:用户手输一个易记密码,而该密码可被基于常见密码训练出的算法猜中。或者,用户输入了我们检测到与其其他网站/应用复用的密码,暴露于潜在“撞库(credential stuffing)”攻击,即攻击者把泄露的邮箱/密码组合拿到其他网站反复尝试。
    • Red:当用户录入已有账号时,输入了已知在数据泄露中受损的邮箱+密码组合。
  2. 用户手输密码(尤其短而熟的密码)非常容易。因此,自动生成密码这一功能必须做得更易用、更易发现。我使用的密码管理器 1Password 提供浏览器插件,这个插件做了几件关键事情:
  • 自动识别密码表单并弹出调用密码管理器的对话框,避免可发现性倒置。
  • 提供“一键生成强密码”,让它比手输更省力,避免可用性倒置。
  • 一些网站有密码约束(如符号、长度要求)。插件允许用户配置这些策略,降低用户退回去手工创建劣质密码的概率。
  1. 我们自己的数据库可以包含“已验证安全域名”“发生过数据泄露的域名”,以及第三方已提供的钓鱼域名。还可以把域名映射到“已泄露密码列表”。再进一步,结合上文提到的密码策略,它甚至可以记录各域名有哪些限制。下面是几个潜在用例:
  • 当用户被要求在疑似钓鱼域名输入用户名和密码时,弹出红色警告框。
  • 当用户即将把凭据提交给一个未验证域名,且该域名并非这些凭据最初保存对应的域名时,弹出额外警告。
  • 当用户的用户名/密码可能在数据泄露中暴露时发出警告,包括其在其他网站和应用中复用该密码的情形。
  • 当足够多用户在某域名上生成带特定限制的密码时,系统可记录并推断该站点存在该类限制;后续用户可自动生成满足这些约束的密码。 当然,这些分析本身还不足以下最终决策,但基于这些场景,至少值得认真评估:我们是否应自建一个可扩展数据库,并把它发展为公司的核心竞争力

  1. 2003 年,Rico Mariani 创造了 pit of success 一词,用来描述“帮助用户自然落入成功实践”的设计理念。我非常喜欢这个说法。 ↩︎

最后更新于