编译 | 燕珊、核子可乐
最近,技术圈掀起了一波关于“怎么减少代码认知负荷”的热烈讨论。“导火索”是这样一篇文章:《Cognitive load is what matters》(《认知负荷才是关键》),它从开发者的“脑容量”讲起,阐述了为什么写代码最重要的事,不是用什么高级架构,不是追逐酷炫技术,而是别让别人看你的代码后“想骂人”。文章在 Hacker News 上迅速占据热榜,引发了技术社区的广泛共鸣。
作者 Artem Zakirullin 是一位经验丰富的软件架构师,拥有超过 13 年的软件开发和工程经验。他目前担任软件开发公司 Inktech 的 CTO 一职。
OpenAI 创始成员之一、前特斯拉 AI 主管 Andrej Karpathy 在 X 平台上转发了这篇文章,发出感慨评价:“这大概是最真实却最少被实践的观点。”
并且,埃隆·马斯克也在 Karpathy 的帖子下评论道:“确实。”
认知负荷为什么如此重要
文章提出了一个人人都能感受到但少有人正视的问题:写代码时的“认知负荷”(Cognitive Load)。
所谓认知负荷,是指开发人员为了完成一项任务所需要承担的思考量。也可以简单理解为:为了完成一项任务,需要动多少脑子、占用多少注意力。
特别是当我们阅读代码时,需要将变量值、控制流逻辑和调用序列等内容塞进自己的脑袋。普通人能够支配的记忆力大约可以容纳 4 个这样的构建块,而一旦认知负荷达到了这个阈值,对于事物的理解难度就会开始陡然上升。
想象一下,你需要接手一个完全陌生的项目并进行修复。当你打开代码,迎面而来的是前一位“天才开发者”留下的大量酷炫的架构设计、花哨的库以及时下流行的技术方案。这种情况下,你不仅要理解项目本身的逻辑,还得费力去消化这些复杂的设计决策——这就是典型的高认知负荷。
Artem Zakirullin 进一步将认知负荷分为两种:
内在负荷:源自任务固有的难度,这种没法避免,也是软件开发工作的核心所在。
外在负荷:由信息的呈现方式导致,与任务本身无关,例如聪明作者们的个人习惯。这方面负荷是可以大大降低的。
因此, Zakirullin 在文章中的核心主张就是,尽量减少项目中的外在认知负荷。
“减负秘笈”
那么,如何减少外在认知负荷?结合开发中常见的“认知雷区”例子,Zakirullin 给了一些接地气的建议:
1. 复杂条件语句:拜托别玩脑筋急转弯
这类代码你可能见过:
if val > someConstant // 🧠+
&& (condition2 || condition3) // 🧠+++,上一条件应当为真,c2 或 c3 之一必须为真
&& (condition4 && !condition5) { // 🤯,到这一步我们的脑袋就快爆炸了……
...
}
解决办法?引入清晰的中间变量:
isValid = val > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5
// 🧠,有了描述性变量,我们就不再需要记住条件
if isValid && isAllowed && isSecure {
...
}
2. 继承噩梦:少用继承,多用组合
“继承链”是认知负荷的天花板。你改 AdminController 的代码,发现它继承了 UserController,后者又继承了 GuestController,还不止这些——改着改着,突然冒出来个 SuperuserController,让你意识到一处改动可能引发蝴蝶效应。
解决办法?别玩继承了,多试试组合吧。
3. 小模块≠简单:浅模块问题
开发界流传着一种迷思,像“方法应该少于 15 行代码”或者“类不能太大”之类的常规习惯实际上是错的。
事实证明,结果一不小心,你的项目里塞满了多个小模块,那维护起来比修一张蜘蛛网还难。
深模块——接口简单,但功能复杂;
浅模块——相对于所提供的小功能,其接口相对复杂。
Zakirullin 认为更好的方法是设计深模块:隐藏内部复杂性,只暴露一个简单的接口。比如 UNIX 的 I/O 接口就非常简单,只有五个基本调用:
open(path, flags, permissions)
read(fd, buffer, count)
write(fd, buffer, count)
lseek(fd, offset, referencePosition)
close(fd)
“信息隐藏非常重要,而浅模块中无法隐藏太多的复杂性。”Zakirullin 在文章中说道。
4. 语言功能越多越好吗
当遇到喜欢的编程语言发布新功能时,谁能不感到兴奋并迫不及待想用起来。然而,文章还提醒说,这种一时热情的背后藏着一个巨大的认知负荷陷阱。
Zakirullin 引用了 Rob Pike(Go 语言的主要设计者之一)的一句话:要通过限制选项的数量来降低认知负荷。
举个例子,当你用了一些新特性写了代码,可能当时觉得很“优雅”。但几个月后,当你回头看这些代码时,你不仅需要重新理解代码逻辑,还得想起“我当时为啥要用这个特性来解决这个问题?”
这无疑增加了外在认知负荷。简言之,丰富的语言功能肯定不是坏事,但前提这些功能要彼此关联。
5. 别滥用分层架构
有人说,架构设计要“优雅”,于是各种抽象层、接口层、适配器层层叠叠。等到真要查 Bug 时,开发者得顺着 5 层调用链一路追溯问题来源,头发掉得比写论文还快。
分层架构的初衷是通过抽象隐藏复杂性。有些人可能以为这样的分层有助于快速替换数据库或者其他依赖项,但事实并非如此。存储方面的变更会导致很多问题。
Zakirullin 强调,开发过程中最不需要担心的就是数据访问层上的抽象机制。充其量,这些抽象能够节约 10% 的迁移时间(这还是在比较乐观的情况下),而真正的麻烦永远来自数据模型不兼容、通信协议、分布式系统挑战以及隐式接口。
所以,既然后续根本没有回报,又何必要为这种分层架构付出高昂的认知负荷成本呢?
总之,分层架构只有在需要明确扩展点时才有意义。否则,这些层次带来的额外认知负荷远高于它能提供的好处。
6. 领域驱动设计(DDD):用对了是神器,用错了是灾难
文章还提到另一个开发者热衷但经常误解的概念:领域驱动设计(DDD)。DDD 本质是关于问题空间的思考,而不是解决方案空间的代码设计。
问题是,许多团队在实践中“跑偏”了。本应专注于领域建模和边界划分的 DDD,变成了某种固定的文件夹结构、服务命名规则、或者对“Repository 模式”的机械化崇拜。
每个人对于 DDD 的理解和解释方式都可能独特且主观。如果基于这样的理解来编写代码,则必然会创造出大量非必要的认知负荷——相当于给后续接手的开发人员埋下一颗颗炸雷。
7. 熟悉项目 VS 认知负荷
认知负荷高低,决定了一个项目对开发者有多“友好”。熟悉项目的开发者,已经把代码逻辑内化到脑子里,干活轻松省事;但对新人来说,如果项目充满谜语一样的命名、复杂的模块关系和稀缺的文档,他们很快就会懵圈,效率直线下降。
文章给了一个建议:每当有新人加入项目时,请尝试衡量他们的懵圈程度。如果他们花了 40 分钟还是一头雾水,那肯定意味着代码中存在需要改进的地方。
而如果能将认知负荷始终保持在较低水平,那么新人在入职后的几个小时内就可以为代码库做出贡献。
写在最后
减少认知负荷并不是一个高深的哲学,而是开发者日常需要实践的基本功。或许,每次写代码时,我们都该问问自己:
“这段代码,是帮同事省脑子,还是让他们掉头发?”
声明:本文为 InfoQ 翻译,未经许可禁止转载。
4000520066 欢迎批评指正
All Rights Reserved 新浪公司 版权所有