本文围绕 认知负荷(Cognitive Load)这一核心概念展开,指出开发者在编写代码时应尽量减少不必要的认知负荷,从而提升代码的可读性和可维护性。认知负荷是指开发者为完成任务所需的思考量,当认知负荷超出人类工作记忆的容量(约 4 个信息块)时,理解和处理任务会变得困难。文章区分了两种认知负荷:

  • 内在认知负荷:由任务本身的复杂性决定,无法减少。
  • 外在认知负荷:由信息的呈现方式引起,与任务无关,可以通过优化代码设计显著减少。

作者通过多个实际例子和反模式分析,强调了减少外在认知负荷的重要性,并提供了具体的实践建议。

1. 复杂条件与嵌套逻辑

  • 问题:复杂条件和嵌套 if 语句会增加认知负荷,尤其当条件过多时,开发者难以追踪逻辑。
  • 解决方案:引入中间变量或采用早返回(early return)模式,简化逻辑。例如,用有意义的变量名替代复杂条件表达式。

2. 继承的复杂性

  • 问题:深层继承链(如多级 Controller 继承)导致开发者需要逐层理解父类逻辑,容易造成认知过载。
  • 解决方案:优先使用组合(composition)而非继承,以减少类之间的耦合和复杂性。

3. 浅模块与信息隐藏

  • 问题:过多浅层模块(方法、类或微服务)会增加模块间交互的复杂性,反而不利于理解。
  • 解决方案:构建深层模块,提供简单接口但隐藏复杂实现。例如,UNIX I/O 的五个简单调用隐藏了数十万行代码的实现细节。

4. 微服务与分布式单体

  • 问题:过度拆分微服务会导致复杂的依赖关系和高认知负荷,容易形成“分布式单体”。
  • 解决方案:在系统设计初期避免过早引入微服务,优先构建清晰的模块化单体架构(monolith)。仅在团队规模或部署需求迫切时,才逐步拆分服务。

5. 语言特性与框架滥用

  • 语言特性
    • 问题:过多语言特性(如 C++ 的初始化方式)会增加学习成本和认知负荷。
    • 解决方案:减少依赖复杂特性,优先选择简单、直观的实现方式。
  • 框架滥用
    • 问题:过度依赖框架的“魔法”可能导致开发者需要额外学习框架的细节。
    • 解决方案:将业务逻辑与框架分离,框架仅作为工具使用,避免代码与框架深度耦合。

6. HTTP 状态码与自描述错误

  • 问题:使用数字状态码(如 401403)会让开发者和 QA 需要额外记忆这些状态码的具体含义。
  • 解决方案:返回自描述的错误信息(如 "code": "jwt_has_expired"),减少对数字状态码的依赖。

7. DRY 原则的滥用

  • 问题:过度追求代码复用可能导致不必要的耦合,增加认知负荷。
  • 解决方案:适度重复代码,避免引入不必要的抽象和依赖。引用 Rob Pike 的观点:“一点点复制优于一点点依赖”。

8. 分层架构与 DDD 的误用

  • 分层架构
    • 问题:过多抽象层(如 Hexagonal/Onion Architecture)可能增加跳转成本,带来额外的认知负荷。
    • 解决方案:仅在有实际扩展需求时引入抽象层,避免为追求架构模式而增加不必要的复杂性。
  • DDD(领域驱动设计)
    • 问题:误将 DDD 的问题域概念(如领域语言)应用到解决方案域,导致主观性和复杂性增加。
    • 解决方案:专注于 DDD 的问题域部分,避免过度强调技术实现细节。

9. 认知负荷与团队协作

  • 问题:熟悉的代码不等于简单的代码,新人可能因代码复杂性而难以上手。
  • 解决方案:通过新人代码审查、结对编程等方式评估代码的认知负荷,确保新人能快速理解并贡献代码。

10. 总结与原则

  • 减少一切超出任务本身的认知负荷,遵循以下基本原则:
    • 优化代码以降低外在认知负荷。
    • 优先信息隐藏和简单接口。
    • 避免为追求抽象和模式而增加复杂性。
    • 引入抽象层或微服务时,应基于实际需求,而非盲目模仿行业趋势。

原文

认知负荷才是关键

这是一个不断更新的文档,上次更新时间:2024年12月。欢迎大家贡献内容!

引言

市面上充斥着各种流行词汇和最佳实践,但大多数都未能奏效。我们需要更根本的东西,一些不会出错的原则。

有时,我们在阅读代码时会感到困惑。困惑会消耗时间和金钱。而困惑的根源在于过高的认知负荷 (cognitive load)。它并非是什么高深的抽象概念,而是一种基本的人类局限。它真实存在,我们能够切身感受到。

既然我们花费在阅读和理解代码上的时间远多于编写代码,就应该不断反思,我们的代码是否承载了过多的认知负荷。

认知负荷 (cognitive load)

认知负荷是指开发人员为了完成一项任务需要投入思考的程度。

在阅读代码时,你会将变量的值、控制流逻辑和调用顺序等信息放入大脑。普通人的工作记忆中大约能同时存储四个这样的信息块。一旦认知负荷达到这个阈值,理解代码就会变得异常困难。

假设我们需要修复一个完全陌生的项目。据说,一位非常聪明的开发人员参与了这个项目,其中运用了许多酷炫的架构、精巧的库和时髦的技术。换句话说,这位作者为我们制造了巨大的认知负荷。

认知负荷

我们应该尽可能降低项目中的认知负荷。

认知负荷的类型

内在的 (Intrinsic) - 由任务本身的难度引起,是软件开发的核心,无法避免。

外在的 (Extraneous) - 由信息的呈现方式导致,例如作者自作聪明的怪癖等与任务无关的因素。这类认知负荷可以大幅降低,这也是我们关注的重点。

内在 vs 外在

接下来,我们直接来看一些外在认知负荷的具体例子。

我们用以下符号来表示认知负荷的程度:
🧠:工作记忆清新,零认知负荷
🧠++:工作记忆中存在两个事实,认知负荷增加
🤯:认知过载,需要记住超过 4 个事实

虽然我们的大脑远比这复杂且难以捉摸,但这个简化模型还是可以说明问题的。

复杂的条件语句

if val > someConstant // 🧠+
    && (condition2 || condition3) // 🧠+++, 前面的条件必须为真,c2 或 c3 之一必须为真
    && (condition4 && !condition5) { // 🤯, 到这里已经彻底混乱了
    ...
}

不妨引入一些有意义的中间变量:

isValid = val > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5
// 🧠, 我们不需要记住具体条件,因为有描述性的变量
if isValid && isAllowed && isSecure {
    ...
}

嵌套的 if 语句

if isValid { // 🧠+, 只有当输入有效时才执行嵌套代码
    if isSecure { // 🧠++, 只有当输入有效且安全时才执行代码
        stuff // 🧠+++
    }
}

对比一下使用提前返回语句 (early returns) 的方式:

if !isValid
    return

if !isSecure return

// 🧠, 我们不必关心之前的返回,如果代码执行到这里,说明一切正常

stuff // 🧠+

我们可以只关注正常执行路径,从而将工作记忆从各种前提条件中解放出来。

继承的噩梦

假设我们被要求为管理员用户修改一些功能:🧠

AdminController extends UserController extends GuestController extends BaseController

哦,部分功能在 BaseController 中,我们先看看:🧠+
基本角色机制在 GuestController 中引入:🧠++
然后在 UserController 中进行了部分修改:🧠+++
终于到了 AdminController,开始写代码吧!🧠++++

等等,还有一个 SuperuserController 继承自 AdminController。修改 AdminController 可能会破坏继承类中的功能,所以我们先深入研究 SuperuserController🤯

比起继承,更应该倾向于使用组合。这里不再赘述,网上有大量相关资料

过多的细小方法、类或模块

在此,方法、类和模块可以互换使用。

诸如“方法不应超过 15 行代码”或“类应该很小”之类的格言,最终都被证明有些失误。

深层模块 (Deep module) - 接口简单,功能复杂。
浅层模块 (Shallow module) - 接口相对于其提供的功能而言过于复杂。

深层模块

过多的浅层模块会增加理解项目的难度。**我们不仅要记住每个模块的职责,还要记住它们之间的所有交互**。要理解一个浅层模块的作用,我们首先需要了解所有相关模块的功能。🤯

信息隐藏至关重要,而浅层模块并没有隐藏太多复杂性。

我有两个业余项目,代码量都在 5K 行左右。第一个项目有 80 个浅层类,而第二个项目只有 7 个深层类。我已经一年半没有维护过这两个项目了。

当我重新审视这些项目时,我发现要理清第一个项目中这 80 个类之间的所有交互关系极其困难。我需要重建大量的认知负荷才能开始编写代码。相反,我能够很快理解第二个项目,因为它只有几个接口简单的深层类。

最好的组件是那些功能强大且接口简单的组件。
John K. Ousterhout

UNIX I/O 的接口非常简单,只有五个基本调用:

open(path, flags, permissions)
read(fd, buffer, count)
write(fd, buffer, count)
lseek(fd, offset, referencePosition)
close(fd)

这个接口的现代实现包含数十万行代码,大量的复杂性都隐藏在底层。然而,由于其简单的接口,它依然易于使用。

这个深层模块的例子来自 John K. Ousterhout 的著作 《软件设计哲学》这本书不仅涵盖了软件开发中复杂性的本质,还对 Parnas 的著名论文 《将系统分解为模块的标准》 进行了精彩的解读,两者都非常值得一读。其他相关文章:也许是时候停止推荐《代码整洁之道》了, 小函数有害论

PS:如果你认为我们提倡使用包含过多职责的臃肿上帝对象,那就误解我们了。

浅层模块与单一职责原则 (Single Responsibility Principle)

我们经常会创建很多浅层模块,并遵循诸如“一个模块应该只负责一件事”这样模糊的原则。那么,这个模糊的“一件事”到底是什么?实例化一个对象算一件事,对吧?所以像 MetricsProviderFactoryFactory 这样的类似乎也说得过去。但这类类的名称和接口,往往比它们的实际实现更加令人困惑。这算什么抽象呢?显然出了问题。

在这些浅层组件之间跳转会让人感到疲惫,线性思维更符合人类的习惯。

我们对系统进行更改是为了满足用户和利益相关者的需求,我们有责任对他们负责。

一个模块应该对一个且仅对一个用户或利益相关者负责。

这才是单一职责原则 (Single Responsibility Principle) 的真正含义。简单来说,如果我们在一个地方引入一个 bug,结果引来两个不同的业务人员投诉,那我们就违反了这个原则。它与模块中代码的数量无关。

但即便如此,这种理解仍然可能弊大于利。因为每个人对这个原则的解读都可能不同。更好的方式是,关注它会造成多少认知负荷。记住,一个模块的更改可能引发跨不同业务线的连锁反应,这会让人精神紧张,仅此而已。

过多的浅层微服务

浅层-深层模块原则与规模无关,同样适用于微服务架构。过多的浅层微服务弊大于利,业界正朝着“宏服务”的方向发展,即服务不再那么浅薄。最糟糕且最难解决的问题之一,就是所谓的分布式单体,它通常是过度细粒度的浅层分离所导致的。

我曾经为一家初创公司提供咨询,他们一个由 5 名开发人员组成的团队引入了 17 个 (!) 微服务。结果,他们比计划晚了 10 个月,而且离发布还遥遥无期。每一个新的需求都导致 4 个以上的微服务发生变化,集成调试的难度也急剧增加。无论是上市时间还是认知负荷,都达到了无法接受的程度。🤯

这种方式是解决新系统不确定性的正确方法吗?在初期就划清正确的逻辑边界非常困难。关键在于尽可能晚地做出决策,因为那时候你掌握的信息最多。通过预先引入网络层,我们一开始就使设计决策难以撤销。这个团队唯一的理由是:“大型科技公司 (FAANG) 已经证明微服务架构是有效的”。醒醒吧,别做白日梦了!

Tanenbaum-Torvalds 的辩论认为,Linux 的单体架构是过时的,应该采用微内核架构。的确,从理论和美学的角度来看,微内核架构似乎更胜一筹。然而,从实践的角度来看,三十年后,基于微内核的 GNU Hurd 仍在开发中,而单体 Linux 却无处不在。你现在看到的网页由 Linux 提供支持,你家里的智能茶壶也由 Linux 提供支持,而且是单体 Linux。

一个精心设计、模块隔离良好的单体应用,通常比一堆微服务更灵活,并且维护成本更低。只有当需要独立部署,例如为了扩展开发团队时,才应该考虑在模块之间添加网络层,也就是未来的微服务。

功能丰富的语言

当自己喜欢的编程语言发布新功能时,我们总是会感到兴奋。我们会花时间学习这些新功能,并尝试用它们来构建代码。

如果语言功能过多,我们可能会花半个小时来尝试用不同的功能编写几行代码,这其实是在浪费时间。更糟糕的是,**当你之后再回头看这段代码时,你不得不重新梳理一遍当时的思考过程!**

**你不仅需要理解这个复杂的程序,还需要理解当初程序员为什么要选择使用这些特定的功能来解决问题。** 🤯

这段话出自 Rob Pike 之口。

通过限制选择来减少认知负荷。

只要语言的特性是正交的,就可以使用。

一位拥有 20 年 C++ 经验的工程师的感想 ⭐️

前几天我翻看 RSS 阅读器时,发现 C++ 标签下竟然有三百多篇未读文章。自从去年夏天之后,我就没有读过任何关于 C++ 的文章了,而且我感觉很棒!

我使用 C++ 已经有 20 年了,这几乎占了我人生三分之二的时间。我的大部分经验都集中在处理 C++ 语言中最晦涩难懂的部分,例如各种未定义行为。这些经验不具有复用性,而且现在要全部抛弃掉,感觉有些可怕。

比如,你可能难以想象,|| 这个 Token 在 requires ((!P<T> || !Q<T>))requires (!(P<T> || Q<T>)) 中的含义是不同的。前者是约束析取,后者是传统的逻辑或操作符,它们的行为也不同。

你不能简单地为基本类型分配内存,然后直接使用 memcpy 复制一组字节,而不做额外的操作,否则对象的生命周期不会启动。C++20 之前就是这样。虽然 C++20 修复了这个问题,但语言的认知负荷却增加了。

即使问题得到了修复,认知负荷仍然在不断增加。我需要了解修复了哪些问题、何时修复的,以及之前是什么样的。毕竟,我是一名专业人士。诚然,C++ 提供了良好的向后兼容性,这也意味着你**不得不**面对历史遗留问题。例如,上个月,一位同事就 C++03 中的某些行为向我提问。🤯

C++ 曾经有 20 种初始化方式,后来又添加了统一初始化语法,现在有 21 种了。顺便问一下,还有人记得从初始化列表选择构造函数的规则吗?好像是隐式转换时会选择信息损失最少的那个,但是 *如果* 这个值是静态已知的,那么…… 🤯

这种增加的认知负荷并不是来自实际的业务需求,也不是领域固有的复杂性,而是历史原因造成的 (外在认知负荷)。

我不得不制定一些规则。比如,如果一行代码不那么直观,需要查阅标准文档,那我就最好不要那样写。顺便说一下,C++ 标准文档有 1500 页左右。

我绝不是在抱怨 C++ 这门语言。我喜欢它,只是现在我感到有些疲惫了。

业务逻辑与 HTTP 状态码

后端返回:
  • 401 表示 jwt token 已过期
  • 403 表示访问权限不足
  • 418 表示用户被禁用

前端团队使用后端 API 来实现登录功能,他们需要在脑海中临时记住以下信息:

  • 401 表示 jwt token 已过期 // 🧠+,好吧,暂时记住它
  • 403 表示访问权限不足 // 🧠++
  • 418 表示用户被禁用 // 🧠+++

前端开发人员(但愿如此)会在他们的代码中创建一个数字状态码 -> 含义的映射表,这样后续的开发人员就不必再费力记住这些对应关系了。

接下来,QA 人员可能会问: “嘿,我收到一个 403 状态码,这是 token 过期还是权限不足?” **QA 人员不能直接进行测试,因为他们首先需要重建后端人员创建的认知负荷。**

为什么要把这种自定义映射关系记在脑子里呢?更好的做法是将业务细节从 HTTP 传输协议中抽象出来,直接在响应体中返回自描述的代码:

{
    "code": "jwt_has_expired"
}

前端的认知负荷:🧠 (无需记忆任何内容)
QA 的认知负荷:🧠

同样的规则也适用于各种数字状态码(在数据库或其他地方) - **优先使用自描述的字符串**。现在已经不是 640K 内存的时代了,无需为了节省内存而进行优化。

人们花费大量时间在 401403 之间争论,并且基于自己的心智模型进行决策。新加入的开发人员,需要重新梳理这个思考过程。你或许为你的代码编写了架构决策记录 (ADR),帮助新成员理解决策背后的原因,但最终,这些努力其实意义不大。我们可以将错误分为用户相关或服务器相关,但除此之外,事情就变得很模糊了。

PS:区分“身份验证”和“授权”通常也很费力。可以使用更简单的术语,如“登录”和“权限”来降低认知负荷。

滥用 DRY (Don't Repeat Yourself) 原则

不要重复自己 (Don't Repeat Yourself) - 这是你作为软件工程师学到的第一批原则之一。它根植于我们的思维中,以至于我们无法忍受哪怕几行重复的代码。虽然它通常是一个好的基本原则,但过度使用会导致我们难以承受的认知负荷。

如今,每个人都基于逻辑分离的组件构建软件。这些组件通常分布在多个代码库中,代表着不同的服务。当你试图消除所有重复时,可能会导致不相关的组件之间产生紧密的耦合。结果,一个地方的改动可能会在其他看似无关的领域产生意想不到的后果,也会影响在不影响整个系统的情况下替换或修改单个组件的能力。🤯

事实上,即使在一个模块中,也可能出现同样的问题。你可能会过早地提取通用功能,基于一些可能并不存在的相似性。这会导致不必要的抽象,使得代码难以修改或扩展。

Rob Pike 曾说过:

少量复制胜过少量依赖。

我们不愿重复造轮子的想法太强烈了,以至于我们宁愿导入大型、笨重的库,只为了使用一个自己就能轻松编写的小函数。

你所有的依赖项,都是你的代码。 浏览一些导入库的 10 多层堆栈跟踪,然后找出问题所在(*因为总会出问题*),这是一件非常痛苦的事情。

与框架紧密耦合

框架中存在着许多“魔法”。如果过度依赖框架,**我们就会迫使所有后续的开发人员首先学习这些“魔法”**,这可能需要几个月的时间。尽管框架能够帮助我们快速发布 MVP,但从长远来看,它们往往会增加不必要的复杂性和认知负荷。

更糟糕的是,在遇到不符合框架架构的新需求时,框架可能会变成一种限制。在这种情况下,人们最终会选择 fork 一个框架,并维护自己的自定义版本。试想一下,为了交付任何价值,一个新成员必须构建多少认知负荷 (即学习这个自定义框架)?🤯

我们绝不是提倡一切都从头开始!

我们可以编写一些与框架无关的代码。业务逻辑不应该存在于框架之中,而是应该使用框架提供的组件。将框架置于核心逻辑之外,像使用库一样使用框架。这样一来,新成员就可以从第一天起为项目增加价值,而无需先了解框架的复杂性。

我为什么讨厌框架

分层架构

人们对这些东西总是抱有某种工程上的兴奋感。

我曾经也是六边形架构/洋葱架构的热衷拥护者,曾经在各种项目中使用它,并且鼓励其他团队使用。结果,我们项目的复杂性增加了,仅仅是文件数量就翻了一倍。感觉我们只是在编写大量的胶水代码。面对不断变化的需求,我们不得不在多个抽象层中进行修改,这让人感觉非常繁琐。🤯

抽象的目的应该是隐藏复杂性,而在这里,它只是增加了间接性。在调用链中跳转,查看代码并找出问题,是快速解决问题的关键步骤。使用这种架构进行层解耦,要花费指数级别的额外追踪才能定位到错误发生的地方,并且这些追踪通常是分散的,每个追踪都会占用我们有限的工作记忆。 🤯

这种架构起初感觉很直观,但是每次应用到项目中,它带来的坏处都比好处多。最终,我们放弃了这种做法,转而使用传统的依赖倒置原则 (dependency inversion principle)。无需学习端口/适配器等概念,无需不必要的水平抽象层,也无需额外的认知负荷。

如果你认为这种分层可以让你快速替换数据库或其他依赖项,那就大错特错了。更改存储会导致很多问题,相信我们,为数据访问层提供一些抽象只是你最不担心的事。抽象层最多可以帮你节省 10% 的迁移时间(如果有的话),真正的难题在于数据模型的不兼容性、通信协议、分布式系统的挑战以及隐式接口

当 API 的用户数量足够多时,
你在合约中承诺什么并不重要:
你的系统的所有可观察行为
都会被某人依赖。

我们进行了一次存储迁移,花费了大约 10 个月的时间。旧系统是单线程的,因此发布的事件是按顺序发生的。我们所有的系统都依赖于这种行为。然而,这种行为并非 API 合约的一部分,也没有在代码中体现出来。新的分布式存储无法保证事件的顺序,结果导致事件无序。我们只花了几个小时编写了一个新的存储适配器,却花了接下来 10 个月来处理无序事件和其他问题。现在回想起来,分层能够帮助我们快速替换组件,简直就是一个笑话。

既然这种分层架构并不能在未来带来任何好处,为什么要为此付出高认知负荷的代价呢? 而且,在大多数情况下,替换核心组件的未来永远不会发生。

这些架构并非是根本性的,它们只是对更根本原则的主观、片面的解读。为什么要依赖这些主观的理解呢?我们应该遵循更根本的原则:依赖倒置原则 (dependency inversion principle)、认知负荷和信息隐藏。欢迎参与讨论

不要为了架构而添加抽象层。只有在需要出于实际原因而添加扩展点时,才应该添加它们。抽象层不是免费的,它们会占用我们的工作记忆

领域驱动设计 (DDD)

领域驱动设计 (DDD) 有一些非常好的观点,但是它经常被误解。人们会说“我们用 DDD 编写代码”,这其实有些奇怪,因为 DDD 关注的是问题域 (problem space),而不是方案域 (solution space)。

通用语言、领域、有界上下文、聚合、事件风暴等概念都属于问题域 (problem space)。它们旨在帮助我们理解领域,并找到边界。DDD 使得开发人员、领域专家和业务人员能够使用统一的语言进行高效沟通。然而,我们往往会更关注 DDD 的方案域 (solution space) 方面,例如特定的文件夹结构、服务、存储库等技术,而不是关注 DDD 的问题域 (problem space) 方面。

我们对 DDD 的理解很可能是独一无二且主观的。如果基于这种理解编写代码,也就是创建大量的外部认知负荷,那未来的开发人员就惨了。🤯

示例

这些架构都非常简单且容易理解,任何人都可以轻松掌握。

让初级开发人员参与架构评审,他们会帮助你发现那些过于复杂的地方。

熟悉项目中的认知负荷

问题在于,**熟悉并不等同于简单**。它们给人的感觉非常相似,都可以在代码中轻松移动而无需太多思考,但原因却大相径庭。你使用的每个“聪明”(或者说“自作聪明”)和非惯用的技巧,都会给其他人带来学习成本。一旦他们完成了学习,他们就会觉得使用代码变得容易。因此,你很难认识到如何简化自己已经熟悉的代码。这也是我为什么会尝试让“新来的”人员,在他们变得过于“适应”之前去批评代码!

很可能之前的作者是一点一点地创建了这个巨大的混乱,而不是一下子完成的。所以,你是第一个需要尝试一次性理解这一切的人。

在我的课上,我描述了一个庞大的 SQL 存储过程,我们曾经看到它在一个巨大的 WHERE 子句中有数百个条件。有人问,怎么会有人让它变得如此糟糕?我告诉他们:“当只有 2 或 3 个条件时,再添加一个条件没有区别。当有 20 或 30 个条件时,再添加一个条件也没有区别!”

除了你主动选择去简化,代码库中没有任何“简化力”。简化需要付出努力,而人们往往过于匆忙。

感谢 Dan North 的评论

如果你已经将项目的心智模型内化到了长期记忆中,你就不会感受到较高的认知负荷。

心智模型

需要学习的心智模型越多,新开发人员交付价值所需的时间就越长。

当你让新成员加入你的项目时,尝试衡量他们的困惑程度(结对编程可能会有所帮助)。如果他们连续困惑超过 40 分钟左右,那么你的代码肯定有需要改进的地方。

如果保持较低的认知负荷,人们可以在加入公司的头几个小时内为你的代码库做出贡献。

结论

假设我们在第二章中得出的结论实际上是错误的。如果是这样,那么我们刚刚否定的结论,以及我们在前一章中接受的有效结论,可能也都是不正确的。🤯

你感受到了吗?你不仅需要在文章中来回跳转才能理解其含义(浅层模块!),而且这句话本身也很难理解。我们刚刚在你的脑海中制造了不必要的认知负荷。不要对你的同事做这种事。

聪明的作者

我们应该减少所有超出工作固有要求的认知负荷。