Karpathy力薦博客:寫代碼的時候,請心疼一下讀代碼的同事
今天上午,著名 AI 科學家 Andrej Karpathy 在 X 上分享的一篇文章引起了廣泛關(guān)注和討論。這篇文章的核心論點是「認知負荷很重要」,即在寫代碼時,應(yīng)該考慮之后閱讀者和維護者能否更輕松地理解這些代碼。Karpathy 認為「這可能是最真實,但最少被實踐的觀點?!巩吘瓜喈敹嚅_發(fā)者都樂于在自己的項目或工作中「炫技」,甚至以花哨復(fù)雜、難以理解為榮。
很多讀者對此表示了認同,并分享了自己的觀點和經(jīng)歷。
Hyperbolic 聯(lián)合創(chuàng)始人及 CTO Yuchen Jin 順勢分享了一本書《軟件設(shè)計的哲學》。他指出:「復(fù)雜性是軟件的主要敵人。」這本書將復(fù)雜性定義為:軟件系統(tǒng)結(jié)構(gòu)中任何會使系統(tǒng)難以理解和修改的東西。而認知負荷是復(fù)雜性的一個重要因素。
開發(fā)者 Aryan Agal 給出了一個更為具體的建議:避免循環(huán)代碼調(diào)用,讓代碼的結(jié)構(gòu)像樹一樣。
langwatch.ai 開發(fā)者 Rogerio Chaves 則吐嘈說:最喜歡增加別人認知負荷的是中級開發(fā)者,初級和高級開發(fā)者都會盡力讓自己的代碼清晰明白,目標就僅僅是解決問題。
也有人思考 AI 編程中的認知負荷問題。
不過,也有人表示,聰明開發(fā)者在代碼中炫的技其實很有趣。
以下是這篇文章的中文版。文章作者為軟件開發(fā)與服務(wù)公司 Inktech 的 CTO Artem Zakirullin,他同時也是一位資深開發(fā)者。
認知負荷很重要
在軟件開發(fā)領(lǐng)域,有太多的流行詞和最佳實踐了,但讓我們關(guān)注一些最基本的東西吧。真正重要的東西是開發(fā)者在處理代碼時感到的困惑度。
困惑會浪費時間和金錢。困惑是由高認知負荷(cognitive load)引起的。這不是一些花哨的抽象概念,而是一種基本的人類約束。
認知負荷
認知負荷是開發(fā)者為了完成一項任務(wù)所需的思考量。
閱讀代碼時,你會將變量值、控制流邏輯和調(diào)用序列等內(nèi)容放入頭腦中。普通人的工作記憶中大約可以容納四個這樣的塊。
相關(guān)討論:https://github.com/zakirullin/cognitive-load/issues/16
一旦認知負荷達到這個閾值,就很難再理解各種事情。
假設(shè)我們的任務(wù)是修復(fù)一個完全不熟悉的項目。我們被告知該項目的貢獻者包括一個非常聰明的開發(fā)者,他使用了很多炫酷的架構(gòu)、花哨的軟件庫和時髦的技術(shù)。也就是說,那位開發(fā)者給我們造成了高認知負荷。
我們應(yīng)該盡可能減少項目中的認知負荷。
認知負荷的類型
內(nèi)在型:來自任務(wù)本身固有的難度。這種認知負荷無法減少,并且也正是軟件開發(fā)的核心。
外來型:源自信息呈現(xiàn)的方式。這種認知負荷的產(chǎn)生因素與任務(wù)并不直接相關(guān),比如某個聰明開發(fā)者的奇怪癖好。這種認知負荷可以大幅減少。這也是本文關(guān)注的認知負荷。
復(fù)雜條件
if val > someConstant // ??+
&& (condition2 || condition3) // ??+++, 上一個條件應(yīng)該為真,c2 或 c3 之一必須為真
&& (condition4 && !condition5) { // ??, 這個會讓我們的頭腦混亂不清
...
}
引入一些名稱有意義的中間變量
isValid = val > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5// ??, 我們不需要記住這些條件,這里存在描述性變量
if isValid && isAllowed && isSecure {
...
}
繼承的噩夢
當我們需要為我們的管理員用戶更改一些內(nèi)容時:??
AdminController extends UserController extends GuestController extends BaseController
哦,一部分功能在 BaseController 中,讓我們看看:??+
GuestController 中引入了基本的角色機制:??++
UserController 中一部分內(nèi)容被修改了:??+++
最后,AdminController,讓我們編寫代碼吧!??++++(認知負荷越來越高)
哦,等等,還有個 SuperuserController 是對 AdminController 的擴展。如果修改 AdminController,我們會破壞繼承類中的某些東西,所以讓我們首先研究下 SuperuserController:??
優(yōu)先使用組合而不是繼承。這里不會深入詳情,但這個視頻《繼承的缺陷》值得一看:https://www.youtube.com/watch?v=hxGOiiR9ZKg
小方法、類或模塊太多了
在這里,方法、類和模塊的含義是可以互換的。
事實證明,「方法應(yīng)該少于 15 行代碼」或「類應(yīng)該很小」之類所謂的警句是有些錯誤的。
- 深模塊(Deep module)—— 接口簡單,功能復(fù)雜
- 淺模塊(Shallow module)—— 相對于它提供的小功能而言,接口相對復(fù)雜
淺模塊太多會使項目難以理解。我們不僅要記住每個模塊的功能,還要記住它們的所有交互。要了解淺模塊的目的,我們首先需要查看所有相關(guān)模塊的功能。??
信息隱藏至關(guān)重要,并且我們不會在淺模塊中隱藏太多復(fù)雜性。
我有兩個實驗性項目,差不多都有 5K 行代碼。第一個有 80 個淺類,而第二個只有 7 個深類。我已經(jīng)一年半沒有維護過這些項目了。
當我回頭進行維護時,我意識到很難理清第一個項目中這 80 個類之間的所有交互。我必須重建大量的認知負荷才能開始寫代碼。另一方面,我能夠快速掌握第二個項目,因為它只有幾個深類和一個簡單的接口。
正如《軟件設(shè)計的哲學》的作者、斯坦福計算機科學教授 John K. Ousterhout 說的那樣:「最好的組件是那些提供強大功能但接口簡單的組件。」
UNIX I/O 的接口就非常簡單。它只有五個基本調(diào)用:
此接口的現(xiàn)代實現(xiàn)有數(shù)十萬行代碼。許多復(fù)雜性都隱藏在了引擎蓋下。但由于其接口簡單,因此非常易于使用。這個深模塊示例取自《軟件設(shè)計哲學》一書。
特性豐富的語言
當我們最喜歡的編程語言發(fā)布了新特性時,我們會感到興奮。我們會花一些時間學習這些特性,并在此基礎(chǔ)上構(gòu)建代碼。
如果新特性很多,我們可能會花半小時玩幾行代碼,以使用這個或那個特性。這有點浪費時間。但更糟糕的是,當你稍后回來時,你得重新構(gòu)建那個思考過程!
你不僅要理解這個復(fù)雜的程序,你還得理解為什么程序員決定從可用的特性中選擇這種方式來解決問題。
此處引用 Rob Pike 說的一句話:
通過限制選擇的數(shù)量來減少認知負荷。
只要語言特性彼此正交,它們就是可以接受的。
來自一位有 20 年 C++ 經(jīng)驗的工程師的想法
前幾天,我在看我的 RSS 閱讀器時發(fā)現(xiàn),我的「C++」標簽下有三百多篇未讀文章。從去年夏天到現(xiàn)在,我一篇關(guān)于 C++ 語言的文章都沒讀過,感覺好極了!
我使用 C++ 已經(jīng)有 20 年了,它幾乎占了我生命的三分之二。我的大部分經(jīng)驗都是在處理這種語言最陰暗的角落(比如各種未定義的行為)。這些經(jīng)驗并不能重復(fù)使用,而且現(xiàn)在全部扔掉還真有點讓人毛骨悚然。
比如,你能想象嗎,在 requires ((!P<T> || !Q<T>)) 和 requires (!(P<T> || Q<T>)) 中,標記 || 的含義是不同的。前者是約束析取,后者是古老的邏輯或運算符,它們的行為是不同的。
你不能為一個瑣碎的類型分配空間,然后不費吹灰之力就在那里 memcpy 一組字節(jié) —— 這不會啟動對象的生命周期。在 C++20 之前就是這種情況。C++20 解決了這個問題,但這門語言的認知負荷卻有增無減。
盡管問題得到了解決,但認知負荷卻在不斷增加。我應(yīng)該知道修復(fù)了什么,什么時候修復(fù)的,以及修復(fù)前的情況。畢竟我是專業(yè)人士。當然,C++ 擅長遺留問題支持,這也意味著你將面對遺留問題。例如,上個月我的一位同事向我詢問 C++03 中的一些行為。??
有 20 種初始化方式。增加了統(tǒng)一初始化語法?,F(xiàn)在我們有 21 種初始化方式。順便問一下,有人還記得從初始化列表中選擇構(gòu)造函數(shù)的規(guī)則嗎?關(guān)于隱式轉(zhuǎn)換,信息損失最小,但如果值是靜態(tài)已知的,那么...... ??
這種認知負荷的增加并不是由手頭的業(yè)務(wù)任務(wù)造成的。它不是領(lǐng)域的內(nèi)在復(fù)雜性。它只是由于歷史原因而存在(外在認知負荷)。
我不得不想出一些規(guī)則。比如,如果那行代碼不那么明顯,而我又必須記住標準,那我最好不要那樣寫。順便說一句,該標準長達 1500 頁。
我絕不是在指責 C++。我喜歡這門語言。只是我現(xiàn)在累了。
分層架構(gòu)
抽象本應(yīng)隱藏復(fù)雜性,但在這里它只是增加了間接性。從一個調(diào)用跳轉(zhuǎn)到另一個調(diào)用,以便讀取并找出出錯和遺漏的地方,這是快速解決問題的重要要求。由于這種架構(gòu)的層解耦(uncoupling),需要指數(shù)級的額外跟蹤(通常是不連貫的)才能找到故障發(fā)生點。每一個這樣的跟蹤都會占用我們有限的工作記憶空間。??
這種架構(gòu)起初很有直覺意義,但每次我們嘗試將其應(yīng)用到項目中時,都是弊大于利。最后,我們放棄了這一切,轉(zhuǎn)而采用古老的依賴倒置原則。沒有需要學習的端口 / 適配器術(shù)語,沒有不必要的水平抽象層,沒有無關(guān)的認知負擔。
如果你認為這樣的分層可以讓你快速替換數(shù)據(jù)庫或其他依賴關(guān)系,那就大錯特錯了。改變存儲會帶來很多問題,相信我們,對數(shù)據(jù)訪問層進行抽象是最不需要擔心的事情。抽象最多只能節(jié)省 10% 的遷移時間(如果有的話),真正的痛苦在于數(shù)據(jù)模型不兼容、通信協(xié)議、分布式系統(tǒng)挑戰(zhàn)和隱式接口。
因此,如果將來沒有回報,為什么要為這種分層架構(gòu)付出高認知負荷的代價呢?
不要為了架構(gòu)而增加抽象層。只要出于實際原因需要擴展點,就應(yīng)該添加抽象層。抽象層不是免費的,它們需要占用我們有限的工作記憶。
領(lǐng)域驅(qū)動設(shè)計(DDD)
領(lǐng)域驅(qū)動設(shè)計有一些很好的觀點,盡管它經(jīng)常被曲解。人們說「我們用領(lǐng)域驅(qū)動設(shè)計來寫代碼」,這有點奇怪,因為領(lǐng)域驅(qū)動設(shè)計是關(guān)于問題空間的,而不是關(guān)于解決方案空間的。
無處不在的語言、領(lǐng)域、有邊界的上下文、聚合、事件風暴都是關(guān)于問題空間的。它們旨在幫助我們了解有關(guān)領(lǐng)域的見解并抽象出邊界。DDD 使開發(fā)人員、領(lǐng)域?qū)<液蜆I(yè)務(wù)人員能夠使用統(tǒng)一的語言進行有效溝通。我們往往不關(guān)注 DDD 的這些問題空間方面,而是強調(diào)特定的文件夾結(jié)構(gòu)、服務(wù)、資源庫和其他解決方案空間技術(shù)。
我們解釋 DDD 的方式很可能是獨特而主觀的。如果我們在這種理解的基礎(chǔ)上構(gòu)建代碼,也就是說,如果我們創(chuàng)造了大量無關(guān)的認知負荷,那么未來的開發(fā)人員就注定要失敗。
示例
- 我們的架構(gòu)是標準的 CRUD 應(yīng)用程序架構(gòu),是 Postgres 基礎(chǔ)上的 Python 單體應(yīng)用:https://danluu.com/simple-architectures/
- Instagram 如何在僅有 3 名工程師的情況下將用戶數(shù)量擴展到 1400 萬:https://read.engineerscodex.com/p/how-instagram-scaled-to-14-million
- 我們覺得「哇,這些人真是聰明絕頂」的公司大部分都失敗了:https://kenkantzer.com/learnings-from-5-years-of-tech-startup-code-audits/
- 連接整個系統(tǒng)的一個功能。如果你想知道系統(tǒng)是如何工作的,那就去讀讀吧:https://www.infoq.com/presentations/8-lines-code-refactoring/
這些架構(gòu)非??菰铮埠苋菀桌斫?。任何人都可以輕松掌握。
讓初級開發(fā)人員參與架構(gòu)審查。他們會幫助你找出需要花費腦力的地方。
熟悉項目中的認知負荷
如果你已經(jīng)將項目的心智模型內(nèi)化到了你的長期記憶中,你就不會體驗到高認知負荷。
需要學習的心智模型越多,新開發(fā)人員實現(xiàn)價值所需的時間就越長。
新人加入項目后,請嘗試衡量他們的困惑程度(結(jié)對編程可能會有所幫助)。如果他們的困惑時間連續(xù)超過 40 分鐘,那么你的代碼中就有需要改進的地方。
如果你能保持較低的認知負荷,新人就能在加入公司的幾個小時內(nèi)為你的代碼庫做出貢獻。
結(jié)論
試想一下,我們在第二章中的推論實際上并不正確。如果是這樣的話,那么我們剛剛否定的結(jié)論,以及前一章中我們認為有效的結(jié)論,可能也不正確。
你感覺到了嗎?你不僅要在文章中跳來跳去才能理解其中的意思(淺模塊),而且整個段落也很難理解。我們剛剛給你的大腦造成了不必要的認知負擔。不要這樣對待你的同事。
我們應(yīng)該減少任何超出工作本身的認知負荷。
對于認知負荷,你有什么看法呢?