惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Full Disclosure
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
The GitHub Blog
The GitHub Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Recorded Future
Recorded Future
Y
Y Combinator Blog
Cloudbric
Cloudbric
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Schneier on Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cybersecurity and Infrastructure Security Agency CISA
TaoSecurity Blog
TaoSecurity Blog
Security Latest
Security Latest
N
News and Events Feed by Topic
Hacker News - Newest:
Hacker News - Newest: "LLM"
D
DataBreaches.Net
Security Archives - TechRepublic
Security Archives - TechRepublic
H
Hacker News: Front Page
C
Cisco Blogs
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
Recent Commits to openclaw:main
Recent Commits to openclaw:main
V
Vulnerabilities – Threatpost
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
A
About on SuperTechFans
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Jina AI
Jina AI
C
CXSECURITY Database RSS Feed - CXSecurity.com
Schneier on Security
Schneier on Security
T
Tenable Blog
N
News and Events Feed by Topic
W
WeLiveSecurity
有赞技术团队
有赞技术团队
AI
AI
爱范儿
爱范儿
大猫的无限游戏
大猫的无限游戏
Latest news
Latest news
T
The Blog of Author Tim Ferriss
S
Security Affairs
Know Your Adversary
Know Your Adversary
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
Google DeepMind News
Google DeepMind News
The Cloudflare Blog

文章 - 硬盘在歌唱 on 硬盘在歌唱

Open Prompt:AI 时代的 PR 协作新范式 记一例浮点数精度问题 AI 辅助编程时代,哪些编程语言会更流行? 父子对谈30则 都2024了,你还没随身带录音笔吗? 写段子的窍门 给TiDB(MySQL)写一个代理网关 论怎么与基层干部打成一片 怎么做一个匿名论坛 适合程序员的桌面窗口管理方案 Go语言泛型初体验 双中心主从模式 TrueTime和原子钟 价值6万元的TiDB Hackathon创意 价值10万元的TiDB Hackathon创意 TiDB1024谜题解题报告 Paxos从入门到学会Raft 一个小故事,关于科幻,关于老师 beancount 复式记账实践
从认知心理学看代码可读性
2025-10-15 · via 文章 - 硬盘在歌唱 on 硬盘在歌唱

最近我发现,讨论代码可读性时,认知心理学提供了一个很好的视角。那些我们熟知的最佳实践背后都有共同的心理学基础。

代码是给人读的,而人脑处理信息的方式是有规律的。理解这些规律,就能明白为什么有些代码读起来轻松,有些代码读起来费劲;就能在面对具体问题时做出更好的判断,而不是机械地遵循教条。

认知负荷理论:理解代码复杂度的本质

认知负荷理论(Cognitive Load Theory)由 John Sweller 在 1980 年代提出,最初用于教学设计。核心观点很简单:人的工作记忆容量有限,当认知负荷超过容量时,学习和理解效率会急剧下降。

三种认知负荷

这个理论把认知负荷分成三种:**内在负荷(Intrinsic Load)**来自问题本身的固有复杂度,**外在负荷(Extraneous Load)**来自信息呈现方式不当带来的额外负担,**相关负荷(Germane Load)**来自构建和强化心智模型的过程。这三种负荷有个总的限制:内在负荷 + 外在负荷 + 相关负荷 ≤ 工作记忆容量

应用到读代码的场景:内在负荷对应问题本身的复杂度,比如分布式事务的一致性、复杂的业务逻辑;外在负荷对应代码写得烂带来的额外负担,比如糟糕的命名、混乱的结构、不一致的风格;相关负荷对应理解代码的过程,包括理解抽象、建立心智模型、学习领域知识。

外在负荷完全可以消除,且应该尽量消除。这是我们优化代码可读性的主战场。同样的逻辑,写法不同,理解难度可能相差几倍。而相关负荷是"好的"负荷,好代码应该帮助读者高效地建立正确的心智模型。如果外在负荷太高,留给理解真正问题的容量就不够了。所以好代码的目标是最小化外在负荷,优化相关负荷。

为什么我们需要抽象

在读代码这个任务上,内在负荷并不是一成不变的。同样的需求可以有不同的设计,不同的设计带来不同的理解问题的视角。一个好的抽象能让复杂问题变简单,从而降低内在负荷。

举个例子:假设你要管理一堆相互依赖的状态变化,直接去处理这些状态转换会很复杂(高内在负荷)。但如果引入"状态机"这个概念来理解,问题就清晰多了。虽然学习"状态机"需要一些时间(相关负荷),但理解问题本身变简单了(内在负荷降低)。这就是好的抽象的价值:用相关负荷(学习抽象)来换取更低的内在负荷(问题变简单了)

从代码可读性的角度看,这解释了为什么设计模式有价值。它们提供理解复杂问题的"心智工具",降低问题的内在复杂度。比如 Observer 模式让"多对多的依赖关系"变得容易理解。领域驱动设计(DDD)的价值也在这里:找到合适的领域抽象,让复杂的业务逻辑从"一团乱麻"变成"清晰的概念组合"。当然,设计模式和DDD还有其他方面的价值,这里只讨论它们对代码可读性的贡献。

理解了内在负荷和相关负荷可以相互转化,我们就能明白什么是好抽象,什么是过度抽象。好抽象是降低的内在负荷大于引入的相关负荷,总认知负荷降低了。而过度抽象恰恰相反:内在负荷没降低,相关负荷却增加了,比如把简单的 5 行代码拆成 3 个函数需要跳来跳去才能理解,为了"可扩展性"引入根本用不到的复杂设计模式,为了避免"可能的"重复搞出过于通用的抽象。

工作记忆容量限制:代码可读性的核心挑战

认知心理学的一个有趣的发现是 Miller’s Law(7±2 法则),它告诉我们:人的工作记忆容量有限,一次只能处理 5-9 个信息单元。

这个工作记忆有点像 CPU 的寄存器:容量很小,但处理速度很快。而长期记忆更像硬盘:容量大,但访问速度慢。读代码时,我们主要依赖工作记忆来理解逻辑,如果工作记忆装不下,就得频繁去"硬盘"(长期记忆)里翻找之前看过的内容,效率就低了。

读代码时,我们需要在脑海中记住:变量的值、状态、分支条件、上下文等。如果超过了工作记忆容量,就会忘记前面的内容,需要回头翻看,思维流程被打断,理解效率急剧下降。

这法则可以用来解释许多常见的代码编写规范。

比如一个函数不应该太长。原因很简单:信息太多时,工作记忆不够用,读到后面就忘了前面的内容,需要回头翻看。

再比如为什么要拆分函数,为什么要封装类。它们的作用就是信息压缩,把占用多个工作记忆单元的信息压缩成 1 个信息单元,显著减少工作记忆占用。

命名为什么如此重要?当命名足够准确时,我们只要看到变量名或者函数名就能直接判断它的行为,这样可以做到完全不占用工作记忆。

不要使用过深的嵌套。这是因为在阅读过程中每进入一层嵌套时,外层的信息作为 context 需要保存在工作记忆中,嵌套过深很容易导致工作记忆不够用。而 early return 模式能很好的解决这个问题,它的价值在于:处理完某种情况后立即 return,该分支占用的记忆单元被释放,不用继续记住

互相调用的函数要放在一起。一方面避免读到调用函数的时候,被调用的函数信息已经因为工作记忆不够用而忘记了;另一方面被调用的函数读完很快就被用掉了,可以安全地从工作记忆中释放掉,快速腾出空间。

其他的还有诸如减少变量的作用域、函数参数列表不要过长、变量的定义和使用不要离太远,都是类似的道理,不多说了。

格式塔理论

格式塔揭示了代码格式的重要性:大脑会自动把零散信息组织成有意义的整体。我们可以利用这个特性来减少工作记忆占用。

比如接近性原则:大脑会自动根据东西的间隔将它们分组。我们在编码代码时相关代码放在一起,用空行分隔不同的逻辑组。效果很明显:

 1// 没分组:20 个独立信息单元
 2func handleRequest(req Request) {
 3    validateAuth(req)
 4    validateInput(req)
 5    validatePermission(req)
 6    user := getUser(req.UserID)
 7    data := fetchData(req.DataID)
 8    cache := checkCache(data.ID)
 9    result := process(user, data, cache)
10    saveResult(result)
11    updateCache(result)
12    sendNotification(user)
13    logRequest(req)
14    updateMetrics(req)
15}
16
17// 分成 4 组:只需记住 4 个逻辑单元
18func handleRequest(req Request) {
19    // Validation
20    validateAuth(req)
21    validateInput(req)
22    validatePermission(req)
23    
24    // Data fetching
25    user := getUser(req.UserID)
26    data := fetchData(req.DataID)
27    cache := checkCache(data.ID)
28    
29    // Processing
30    result := process(user, data, cache)
31    saveResult(result)
32    updateCache(result)
33    
34    // Post-processing
35    sendNotification(user)
36    logRequest(req)
37    updateMetrics(req)
38}

大脑看到后者时会自动识别出"四个阶段",而不是"12 行代码"。这就是信息压缩。

还有相似性原则,这是说大脑很擅长识别相似的模式,保持一致的命名风格、代码结构,大脑就能识别出"这是同一模式"。

 1// 一致的模式:大脑只需记住一个模式
 2getUserName(id)
 3getUserAge(id)
 4getUserEmail(id)
 5getUserAddress(id)
 6
 7// vs 混乱的风格:每个都要单独理解
 8getUserName(id)
 9fetch_user_age(id)
10GetUserEmail(id)
11user_address(id)

这也是为什么代码规范很重要:不是为了好看,是为了降低认知负担。

重复代码的可接受性

说到这里,我想讨论一个有争议的话题:重复代码。

传统观点认为重复是坏的(DRY 原则),但从工作记忆的角度看,并非所有重复都是坏的

工整的重复代码,虽然看起来有一大片,但因为模式相同,实际上不占用多个心智单元。大脑很擅长识别模式:“哦,这 5 段代码都一样,只是参数不同”。这种理解成本远低于"理解一个复杂的抽象 + 在多个文件间跳转"。

 1// 工整的重复:一个模式,容易理解
 2result1 := validate(data1)
 3if result1.Error != nil {
 4    return result1.Error
 5}
 6
 7result2 := validate(data2)
 8if result2.Error != nil {
 9    return result2.Error
10}
11
12result3 := validate(data3)
13if result3.Error != nil {
14    return result3.Error
15}
16
17// vs 抽象后:需要理解 helper,跳转查看定义
18results := validateAll([]Data{data1, data2, data3})
19for _, r := range results {
20    if r.Error != nil {
21        return r.Error
22    }
23}

哪个更容易理解?很多时候是前者。

什么样的重复可以接受?简单、工整、模式清晰的重复,次数不多,每次都很直观不需要跳转就能看懂。这样的重复虽然看起来有一大片代码,但因为模式相同,大脑只需要识别一个模式,不会占用多个工作记忆单元。相反,复杂逻辑的重复、大量重复(10+ 次)、不工整的重复(每次都有细微差异需要仔细对比)就不能接受了,这会增加维护成本和认知负担。

DRY 是手段,不是目的。目的是降低维护成本和认知负担。有时候一点重复比"引入抽象 + 跳转理解"占用更少的工作记忆。

双重编码理论:视觉与语义的协同

最后聊一个有意思的理论:双重编码理论(Dual Coding Theory)。

Allan Paivio 发现:人脑有两个独立但相互关联的信息处理通道。

  • 语言/语义通道:处理文字、符号、逻辑含义
  • 视觉/空间通道:处理形状、布局、空间关系

这个理论的关键洞察是:两个通道同时工作,当它们相互加强相互印证时,理解效率翻倍;但如果两个通道传递矛盾信息,就会造成认知混乱。

例如很多代码 format 工具都会把成员初始化的冒号(:)和批量赋值的等号(=)进行对齐,这些不是仅仅为了美观的“表面功夫”,而是可以理解成在用视觉通道传递信息,用对齐表达“一组相似的操作”,减少认知负担。

这个理论也可以用来解释 Yoda notation 为什么不好读。

1// 糟糕:视觉顺序和语言顺序冲突
2if (NULL == ptr) {
3    // ...
4}
5
6// 好:视觉顺序 = 语言顺序
7if (ptr == NULL) {
8    // ...
9}

语言通道理解:“当 ptr 为空时”,而视觉上从左到右是"空 等于 ptr",顺序是反的。两个通道传递的顺序冲突,就会增加认知负担,让人读起来总觉得哪里怪怪的。

再举个例子:视觉通道看到"一组相似的名字",语义通道就会期待"一组相似的行为",即相似的外观应该对应相似的语义,如果语义不符合这个期待,就会产生认知冲突。

 1// 好:看起来像,做的事也像
 2getUserName()      // 从缓存读
 3getUserAge()       // 从缓存读
 4getUserEmail()     // 从缓存读
 5
 6// 糟糕:看起来像,但行为不一致
 7getUserName()      // 从缓存读
 8getUserAge()       // 从缓存读
 9getUserAddress()   // 发起网络请求查数据库!
10
11// 也很糟糕:都是 get 开头,副作用不一致
12getUser()          // 纯读取
13getConnection()    // 创建连接(有副作用)

看起来相似的名字,却有完全不同的性能特征或副作用,这种不一致会让人掉坑里。

双重编码理论往深了说可能有点“玄学”,或者说对认知的理解可能是在潜意识层面了。比如大脑看到较长的一段代码就会下意识认为它的逻辑是复杂的、运行是慢的,包括潜意识会认为名字长的函数会做更复杂的事情。不过咱们日常编程也不必要考虑这么多,就不细说了。

总结

略。