認知負擔設計方法論 - 降低程式碼閱讀成本的設計原則
DRY、SOLID、Clean Code、設計模式……書讀了不少,原則背了很多,但還是常常看別人的程式碼頭痛,自己寫完一個月後也看不懂自己寫的是什麼。
這些原則背後共同指向的是什麼?
在推進一個書庫管理 App 的開發過程中,我們開始認真思考這個問題。結論很簡單:所有程式碼設計原則的終極目標,都是降低閱讀者的認知負擔。
一旦確立了這個核心目標,很多過去「規定要這樣做」的事情,開始有了真正的理由。
認知負擔是什麼
認知負擔(Cognitive Load)指閱讀者在理解程式碼時,需要同時記住的資訊量。
認知心理學家米勒在 1956 年提出「Miller’s Law」:人類工作記憶一次只能處理約 7±2 個項目。超過這個數量,大腦就開始掙扎——反覆閱讀、做筆記、或者乾脆猜。程式碼讓人難懂,通常是寫法讓閱讀者同時記住太多東西。
認知心理學把認知負擔分成三種:
內在認知負擔(Intrinsic Load),任務本身的固有複雜度。排序演算法有它的複雜度,業務邏輯有它的業務規則,無法消除。
外在認知負擔(Extraneous Load),由不良設計產生的額外複雜度——糟糕的命名、過長的函式、深層巢狀、隱藏的副作用。這些是可以、也應該主動消除的。
相關認知負擔(Germane Load),用於建立心智模型的有益負擔——理解一個設計模式、學習架構決策背後的原因。值得保留。
設計的目標因此很清楚:最小化外在認知負擔,讓閱讀者的認知資源真正用在理解問題本身和建立更深刻的理解。
認知負擔從哪裡來
變數狀態追蹤
每一個區域變數,都是閱讀者需要「記住並追蹤」的一個項目。看看這段程式碼:
1void processOrder(Order order) {
2 var items = order.items;
3 var total = 0.0;
4 var discount = 0.0;
5 var tax = 0.0;
6 var shipping = 0.0;
7 var finalPrice = 0.0;
8 var isValid = true;
9 var errorMessage = '';
10 // 閱讀者需要同時追蹤 8 個變數狀態
11}函式還沒開始做什麼,閱讀者已經被迫記住 8 個變數。改成這樣:
1void processOrder(Order order) {
2 final pricing = _calculatePricing(order);
3 final validation = _validateOrder(order);
4 // 閱讀者只需追蹤 2 個概念
5}數量少了,每個名稱也更清楚地說明了它代表什麼。
呼叫層級追蹤
追蹤呼叫鏈需要「堆疊」記憶,就像遞迴一樣,每深入一層就要把上一層的狀態記住。
當閱讀者必須追蹤 methodA -> methodB -> methodC -> methodD 四層呼叫才能理解一段邏輯時,工作記憶很快就到了極限。扁平化呼叫結構,讓邏輯盡量在同一個層次展開,能顯著減輕這種負擔。
命名品質
不佳的命名讓閱讀者需要做「翻譯」工作——先猜測 d 是什麼,再猜測 temp 指的是什麼,然後才能開始理解邏輯。
d 改成 discountAmount,process() 改成 calculateTotalPrice(),閱讀者可以節省的翻譯工夫,直接換成理解業務邏輯的空間。
條件分支複雜度
每一個巢狀的 if,都要求閱讀者同時記住所有外層條件。三層巢狀就是同時記住三個條件,並且理解它們的組合關係。
Guard Clause 是最直接的解法:把各種「不符合條件就提前返回」的情況放在函式開頭,讓主要邏輯只有一層縮排。
1// 提前返回,消除巢狀
2if (!condition1) return;
3if (!condition2) return;
4if (!condition3) return;
5
6// 主邏輯,思路清晰,只有一層縮排隱藏的副作用
函式名稱說「取得」,但偷偷修改了資料庫;方法說「計算」,但同時更新了快取。這些隱藏的副作用強迫閱讀者不能只看名稱理解行為,必須讀進每一行實作,才能確保自己真的理解了這個函式做了什麼。
「命令與查詢分離」(Command-Query Separation)的原則在這裡特別有用:查詢只讀取狀態,命令只修改狀態,兩者不混用。
降低認知負擔的五個原則
單一責任。一個函式只做一件事。自我檢查方法:用一句話描述這個函式做什麼,句子裡出現「和」或「並且」,通常就代表它做了超過一件事。
自說明命名。名稱本身就是文件。理想的命名讓閱讀者不看實作也能理解函式的用途、變數的內容、類別的職責。isEmailFormatValid 比 isValid 好,onUserLoginSuccess 比 handle 好,calculateMonthlyRevenue 比 calc 好。
避免副作用。函式的行為應該和名稱一致、可以預測。修改輸入以外的任何東西——全域狀態、資料庫、快取——都應該在名稱中反映,或者提取為獨立的命令函式。
扁平化結構。巢狀深度每增加一層,都在要求閱讀者多記住一層上下文。1-2 層巢狀是優良的,3 層開始值得考慮是否能用 Guard Clause 或提取函式來簡化,超過 3 層通常就應該重構了。
資訊就近原則。理解一段程式碼時,如果需要跳到其他三個地方去查定義、查常數、查相關邏輯,認知負擔就在這些跳轉中累積。相關的資訊放在一起,讓閱讀者可以在同一個地方理解完整的概念。
SOLID 原則的另一種解讀
用認知負擔視角回頭看 SOLID,會發現它們有了更直覺的意義。
單一責任原則(SRP) 傳統解釋是「一個類別只有一個改變的原因」。認知負擔的版本是:一個類別只代表一個概念,閱讀者不需要建立多個心智模型。UserRepository 的認知負擔低,UserServiceAndValidatorAndNotifier 的認知負擔高——因為閱讀者必須同時掌握多個不同的概念。
開放封閉原則(OCP) 說對擴展開放、對修改封閉。認知負擔的版本是:新增功能時,不需要理解現有程式碼的全部細節。新增一種折扣類型,如果要讀懂 calculatePrice() 的所有邏輯和現有折扣的互動,認知負擔很高。如果只需要理解 Discount 介面的契約然後實作它,認知負擔就被控制在最小範圍。
介面隔離原則(ISP) 要求介面不應包含使用者不需要的方法。認知負擔的視角:一個胖介面迫使閱讀者掃描並判斷哪些方法與自己相關,這是純粹的額外負擔。
量化認知負擔
主觀感覺「這段程式碼很複雜」很難作為行動依據。我們引入了一個簡單的量化公式:
1認知負擔指數 = 變數數 + 分支數 + 巢狀深度 + 外部依賴數1-5 分優良,6-10 分可接受,11-15 分需要重構,超過 15 分必須立即處理。
這個公式不是精確科學,但它提供了可以討論的共同語言。一個函式的指數算出來是 18,「這段程式碼太複雜」就不再是模糊的感覺,而是有數字支撐的判斷。
同樣的思路也用在任務規劃:一個任務需要同時追蹤超過 7 個概念、跨越 2 個以上的架構層、修改超過 5 個檔案,認知負擔就超標了,應該拆分,而不是強行一次完成。
為了讓人更容易讀
電腦不在意程式碼怎麼寫。一個八層巢狀的函式和一個 Guard Clause 寫法的函式,在機器眼中差異幾乎是零。真正在乎的,是接下來要讀這段程式碼的人——三個月後的自己,剛加入的新同事,凌晨三點在修 bug 的任何人。
把「降低閱讀者的認知負擔」作為首要目標,所有設計決策就有了一致的評估標準:這樣寫,閱讀者需要記住更多還是更少?這樣命名,不讀實作也能理解嗎?這樣拆分,每個部分的概念有沒有變得更單純?