Karpathy力薦部落格:寫程式碼的時候,請心疼一下讀程式碼的同事

机器之心發表於2024-12-26

今天上午,著名 AI 科學家 Andrej Karpathy 在 X 上分享的一篇文章引起了廣泛關注和討論。這篇文章的核心論點是「認知負荷很重要」,即在寫程式碼時,應該考慮之後閱讀者和維護者能否更輕鬆地理解這些程式碼。Karpathy 認為「這可能是最真實,但最少被實踐的觀點。」畢竟相當多開發者都樂於在自己的專案或工作中「炫技」,甚至以花哨複雜、難以理解為榮。

圖片
很多讀者對此表示了認同,並分享了自己的觀點和經歷。

Hyperbolic 聯合創始人及 CTO Yuchen Jin 順勢分享了一本書《軟體設計的哲學》。他指出:「複雜性是軟體的主要敵人。」這本書將複雜性定義為:軟體系統結構中任何會使系統難以理解和修改的東西。而認知負荷是複雜性的一個重要因素。
圖片
開發者 Aryan Agal 給出了一個更為具體的建議:避免迴圈程式碼呼叫,讓程式碼的結構像樹一樣。
圖片
langwatch.ai 開發者 Rogerio Chaves 則吐嘈說:最喜歡增加別人認知負荷的是中級開發者,初級和高階開發者都會盡力讓自己的程式碼清晰明白,目標就僅僅是解決問題。
圖片
也有人思考 AI 程式設計中的認知負荷問題。
圖片
不過,也有人表示,聰明開發者在程式碼中炫的技其實很有趣。
圖片
以下是這篇文章的中文版。文章作者為軟體開發與服務公司 Inktech 的 CTO Artem Zakirullin,他同時也是一位資深開發者。
圖片
認知負荷很重要

在軟體開發領域,有太多的流行詞和最佳實踐了,但讓我們關注一些最基本的東西吧。真正重要的東西是開發者在處理程式碼時感到的困惑度。

困惑會浪費時間和金錢。困惑是由高認知負荷(cognitive load)引起的。這不是一些花哨的抽象概念,而是一種基本的人類約束。

認知負荷

認知負荷是開發者為了完成一項任務所需的思考量。

閱讀程式碼時,你會將變數值、控制流邏輯和呼叫序列等內容放入頭腦中。普通人的工作記憶中大約可以容納四個這樣的塊。

相關討論:https://github.com/zakirullin/cognitive-load/issues/16

一旦認知負荷達到這個閾值,就很難再理解各種事情。

假設我們的任務是修復一個完全不熟悉的專案。我們被告知該專案的貢獻者包括一個非常聰明的開發者,他使用了很多炫酷的架構、花哨的軟體庫和時髦的技術。也就是說,那位開發者給我們造成了高認知負荷。
圖片
我們應該儘可能減少專案中的認知負荷。

認知負荷的型別

內在型:來自任務本身固有的難度。這種認知負荷無法減少,並且也正是軟體開發的核心。

外來型:源自資訊呈現的方式。這種認知負荷的產生因素與任務並不直接相關,比如某個聰明開發者的奇怪癖好。這種認知負荷可以大幅減少。這也是本文關注的認知負荷。
圖片
複雜條件
if val > someConstant // 🧠+
&& (condition2 || condition3) // 🧠+++, 上一個條件應該為真,c2 或 c3 之一必須為真
&& (condition4 && !condition5) { // 🤯, 這個會讓我們的頭腦混亂不清
...
}

引入一些名稱有意義的中間變數
isValid = val > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5// 🧠, 我們不需要記住這些條件,這裡存在描述性變數
if isValid && isAllowed && isSecure {
...
}

繼承的噩夢

當我們需要為我們的管理員使用者更改一些內容時:🧠
AdminController extends UserController extends GuestController extends BaseController

哦,一部分功能在 BaseController 中,讓我們看看:🧠+

GuestController 中引入了基本的角色機制:🧠++

UserController 中一部分內容被修改了:🧠+++

最後,AdminController,讓我們編寫程式碼吧!🧠++++(認知負荷越來越高)

哦,等等,還有個 SuperuserController 是對 AdminController 的擴充套件。如果修改 AdminController,我們會破壞繼承類中的某些東西,所以讓我們首先研究下 SuperuserController:🤯

優先使用組合而不是繼承。這裡不會深入詳情,但這個影片《繼承的缺陷》值得一看:https://www.youtube.com/watch?v=hxGOiiR9ZKg

小方法、類或模組太多了

在這裡,方法、類和模組的含義是可以互換的。

事實證明,「方法應該少於 15 行程式碼」或「類應該很小」之類所謂的警句是有些錯誤的。

  • 深模組(Deep module)—— 介面簡單,功能複雜
  • 淺模組(Shallow module)—— 相對於它提供的小功能而言,介面相對複雜
圖片
淺模組太多會使專案難以理解。我們不僅要記住每個模組的功能,還要記住它們的所有互動。要了解淺模組的目的,我們首先需要檢視所有相關模組的功能。🤯

資訊隱藏至關重要,並且我們不會在淺模組中隱藏太多複雜性。

我有兩個實驗性專案,差不多都有 5K 行程式碼。第一個有 80 個淺類,而第二個只有 7 個深類。我已經一年半沒有維護過這些專案了。

當我回頭進行維護時,我意識到很難理清第一個專案中這 80 個類之間的所有互動。我必須重建大量的認知負荷才能開始寫程式碼。另一方面,我能夠快速掌握第二個專案,因為它只有幾個深類和一個簡單的介面。

正如《軟體設計的哲學》的作者、史丹佛電腦科學教授 John K. Ousterhout 說的那樣:「最好的元件是那些提供強大功能但介面簡單的元件。

UNIX I/O 的介面就非常簡單。它只有五個基本呼叫:
圖片
此介面的現代實現有數十萬行程式碼。許多複雜性都隱藏在了引擎蓋下。但由於其介面簡單,因此非常易於使用。這個深模組示例取自《軟體設計哲學》一書。

特性豐富的語言

當我們最喜歡的程式語言釋出了新特性時,我們會感到興奮。我們會花一些時間學習這些特性,並在此基礎上構建程式碼。

如果新特性很多,我們可能會花半小時玩幾行程式碼,以使用這個或那個特性。這有點浪費時間。但更糟糕的是,當你稍後回來時,你得重新構建那個思考過程!

你不僅要理解這個複雜的程式,你還得理解為什麼程式設計師決定從可用的特性中選擇這種方式來解決問題。

此處引用 Rob Pike 說的一句話:

透過限制選擇的數量來減少認知負荷。

只要語言特性彼此正交,它們就是可以接受的。

來自一位有 20 年 C++ 經驗的工程師的想法

前幾天,我在看我的 RSS 閱讀器時發現,我的「C++」標籤下有三百多篇未讀文章。從去年夏天到現在,我一篇關於 C++ 語言的文章都沒讀過,感覺好極了!

我使用 C++ 已經有 20 年了,它幾乎佔了我生命的三分之二。我的大部分經驗都是在處理這種語言最陰暗的角落(比如各種未定義的行為)。這些經驗並不能重複使用,而且現在全部扔掉還真有點讓人毛骨悚然。

比如,你能想象嗎,在 requires ((!P<T> || !Q<T>)) 和 requires (!(P<T> || Q<T>)) 中,標記 || 的含義是不同的。前者是約束析取,後者是古老的邏輯或運算子,它們的行為是不同的。

你不能為一個瑣碎的型別分配空間,然後不費吹灰之力就在那裡 memcpy 一組位元組 —— 這不會啟動物件的生命週期。在 C++20 之前就是這種情況。C++20 解決了這個問題,但這門語言的認知負荷卻有增無減。

儘管問題得到了解決,但認知負荷卻在不斷增加。我應該知道修復了什麼,什麼時候修復的,以及修復前的情況。畢竟我是專業人士。當然,C++ 擅長遺留問題支援,這也意味著你將面對遺留問題。例如,上個月我的一位同事向我詢問 C++03 中的一些行為。🤯

有 20 種初始化方式。增加了統一初始化語法。現在我們有 21 種初始化方式。順便問一下,有人還記得從初始化列表中選擇建構函式的規則嗎?關於隱式轉換,資訊損失最小,但如果值是靜態已知的,那麼...... 🤯

這種認知負荷的增加並不是由手頭的業務任務造成的。它不是領域的內在複雜性。它只是由於歷史原因而存在(外在認知負荷)。

我不得不想出一些規則。比如,如果那行程式碼不那麼明顯,而我又必須記住標準,那我最好不要那樣寫。順便說一句,該標準長達 1500 頁。

我絕不是在指責 C++。我喜歡這門語言。只是我現在累了。

分層架構

抽象本應隱藏複雜性,但在這裡它只是增加了間接性。從一個呼叫跳轉到另一個呼叫,以便讀取並找出出錯和遺漏的地方,這是快速解決問題的重要要求。由於這種架構的層解耦(uncoupling),需要指數級的額外跟蹤(通常是不連貫的)才能找到故障發生點。每一個這樣的跟蹤都會佔用我們有限的工作記憶空間。🤯

這種架構起初很有直覺意義,但每次我們嘗試將其應用到專案中時,都是弊大於利。最後,我們放棄了這一切,轉而採用古老的依賴倒置原則。沒有需要學習的埠 / 介面卡術語,沒有不必要的水平抽象層,沒有無關的認知負擔。

如果你認為這樣的分層可以讓你快速替換資料庫或其他依賴關係,那就大錯特錯了。改變儲存會帶來很多問題,相信我們,對資料訪問層進行抽象是最不需要擔心的事情。抽象最多隻能節省 10% 的遷移時間(如果有的話),真正的痛苦在於資料模型不相容、通訊協議、分散式系統挑戰和隱式介面。

因此,如果將來沒有回報,為什麼要為這種分層架構付出高認知負荷的代價呢?

不要為了架構而增加抽象層。只要出於實際原因需要擴充套件點,就應該新增抽象層。抽象層不是免費的,它們需要佔用我們有限的工作記憶。

領域驅動設計(DDD)

領域驅動設計有一些很好的觀點,儘管它經常被曲解。人們說「我們用領域驅動設計來寫程式碼」,這有點奇怪,因為領域驅動設計是關於問題空間的,而不是關於解決方案空間的。

無處不在的語言、領域、有邊界的上下文、聚合、事件風暴都是關於問題空間的。它們旨在幫助我們瞭解有關領域的見解並抽象出邊界。DDD 使開發人員、領域專家和業務人員能夠使用統一的語言進行有效溝通。我們往往不關注 DDD 的這些問題空間方面,而是強調特定的資料夾結構、服務、資源庫和其他解決方案空間技術。

我們解釋 DDD 的方式很可能是獨特而主觀的。如果我們在這種理解的基礎上構建程式碼,也就是說,如果我們創造了大量無關的認知負荷,那麼未來的開發人員就註定要失敗。

示例

  • 我們的架構是標準的 CRUD 應用程式架構,是 Postgres 基礎上的 Python 單體應用:https://danluu.com/simple-architectures/
  • Instagram 如何在僅有 3 名工程師的情況下將使用者數量擴充套件到 1400 萬:https://read.engineerscodex.com/p/how-instagram-scaled-to-14-million
  • 我們覺得「哇,這些人真是聰明絕頂」的公司大部分都失敗了:https://kenkantzer.com/learnings-from-5-years-of-tech-startup-code-audits/
  • 連線整個系統的一個功能。如果你想知道系統是如何工作的,那就去讀讀吧:https://www.infoq.com/presentations/8-lines-code-refactoring/

這些架構非常枯燥,也很容易理解。任何人都可以輕鬆掌握。

讓初級開發人員參與架構審查。他們會幫助你找出需要花費腦力的地方。

熟悉專案中的認知負荷

如果你已經將專案的心智模型內化到了你的長期記憶中,你就不會體驗到高認知負荷。
圖片
需要學習的心智模型越多,新開發人員實現價值所需的時間就越長。

新人加入專案後,請嘗試衡量他們的困惑程度(結對程式設計可能會有所幫助)。如果他們的困惑時間連續超過 40 分鐘,那麼你的程式碼中就有需要改進的地方。

如果你能保持較低的認知負荷,新人就能在加入公司的幾個小時內為你的程式碼庫做出貢獻。

結論

試想一下,我們在第二章中的推論實際上並不正確。如果是這樣的話,那麼我們剛剛否定的結論,以及前一章中我們認為有效的結論,可能也不正確。

你感覺到了嗎?你不僅要在文章中跳來跳去才能理解其中的意思(淺模組),而且整個段落也很難理解。我們剛剛給你的大腦造成了不必要的認知負擔。不要這樣對待你的同事。

我們應該減少任何超出工作本身的認知負荷。

對於認知負荷,你有什麼看法呢?

原文連結:https://minds.md/zakirullin/cognitive

相關文章