認知負擔:程式碼設計的核心目的
認知負擔:程式碼設計的核心目的
什麼是認知負擔?
認知負擔(Cognitive Load)是心理學中的概念,指的是人腦在處理資訊時所承受的負擔量。
工作記憶的限制
心理學家 George Miller 在 1956 年提出著名的「7 加減 2」法則:人類的工作記憶一次只能處理約 5 到 9 個項目。
這意味著當你閱讀程式碼時:
- 如果需要同時記住超過 7 個變數的狀態,你會開始混淆
- 如果需要追蹤超過 7 層的呼叫關係,你會迷失方向
- 如果一個函式做超過 7 件事,你會難以理解它的目的
程式碼閱讀中的認知負擔
閱讀程式碼時,以下情況會增加認知負擔:
1# 高認知負擔的程式碼
2def process(d):
3 r = []
4 for i in d:
5 if i[0] > 0 and i[1] != "" and len(i) >= 3:
6 t = i[0] * 2 + len(i[1])
7 if t > 10:
8 r.append((i[2], t))
9 return sorted(r, key=lambda x: x[1], reverse=True)閱讀這段程式碼時,你需要:
- 記住
d是什麼(輸入資料) - 追蹤
r的狀態(結果列表) - 理解
i的結構(至少有 3 個元素的序列) - 計算
t的值(某種加權計算) - 記住過濾條件(三個條件)
- 理解最終排序邏輯
這就是典型的高認知負擔程式碼。
核心論點:所有原則的統一目的
Clean Code 不是「優美」,而是「易讀」
很多人誤解 Clean Code 是追求程式碼的「優美」或「藝術性」。但事實上:
1Clean Code 的真正目標是:讓程式碼能被人類輕鬆理解優美的程式碼如果難以理解,就不是好的程式碼。樸素但清晰的程式碼,遠勝於巧妙但費解的程式碼。
無法讀懂的程式碼沒人會讀
這是一個殘酷的現實:
- 如果程式碼太難讀,維護者會選擇重寫而非修改
- 如果程式碼太難讀,除錯會變成猜測遊戲
- 如果程式碼太難讀,知識無法傳承
DRY、SOLID、命名規範 = 降低認知負擔的不同策略
讓我們重新審視這些經典原則:
| 原則 | 傳統解釋 | 認知負擔視角 |
|---|---|---|
| DRY | 不要重複自己 | 讀者只需要理解一次,減少記憶負擔 |
| 單一職責 | 一個類別只做一件事 | 讀者一次只需要理解一個概念 |
| 開放封閉 | 對擴展開放,對修改封閉 | 讀者不需要理解整個系統就能擴展 |
| 依賴反轉 | 依賴抽象而非具體 | 讀者可以忽略實作細節 |
| 命名規範 | 使用有意義的名稱 | 讀者不需要追溯定義就能理解 |
它們的共同目標都是:降低閱讀者的認知負擔。
認知負擔的來源
1. 需要記住前面發生什麼事
1# 高認知負擔:需要記住 data 經歷了什麼轉換
2data = get_raw_data()
3data = filter_invalid(data)
4data = normalize(data)
5data = enrich(data)
6result = aggregate(data)
7
8# 低認知負擔:每步都有清晰的命名
9raw_data = get_raw_data()
10valid_data = filter_invalid(raw_data)
11normalized_data = normalize(valid_data)
12enriched_data = enrich(normalized_data)
13result = aggregate(enriched_data)2. 需要追蹤變數經歷的轉換
1# 高認知負擔:temp 到底是什麼?
2temp = user_input.strip()
3temp = temp.lower()
4temp = temp.replace(" ", "_")
5temp = re.sub(r'[^a-z_]', '', temp)
6
7# 低認知負擔:每個變數都說明自己是什麼
8trimmed_input = user_input.strip()
9lowercase_input = trimmed_input.lower()
10underscored_input = lowercase_input.replace(" ", "_")
11clean_identifier = re.sub(r'[^a-z_]', '', underscored_input)3. 需要理解隱藏的狀態變化
1# 高認知負擔:process() 會修改什麼?
2class DataProcessor:
3 def process(self):
4 self._validate() # 可能修改 self.errors?
5 self._transform() # 可能修改 self.data?
6 self._save() # 可能修改 self.saved?
7
8# 低認知負擔:回傳值明確說明結果
9class DataProcessor:
10 def process(self) -> ProcessResult:
11 errors = self._validate(self.data)
12 if errors:
13 return ProcessResult(success=False, errors=errors)
14
15 transformed = self._transform(self.data)
16 save_result = self._save(transformed)
17 return ProcessResult(success=True, saved_path=save_result)4. 需要跳轉到其他地方才能理解當前程式碼
1# 高認知負擔:需要跳到 MAGIC_VALUE 的定義
2if score > MAGIC_VALUE:
3 return "pass"
4
5# 低認知負擔:直接說明意圖
6PASSING_SCORE_THRESHOLD = 60
7if score > PASSING_SCORE_THRESHOLD:
8 return "pass"降低認知負擔的原則
原則一:在當下就能理解
好的程式碼不需要讀者記住之前發生的事情:
1# 不好:需要記住 user 是什麼
2def process(user):
3 if user[0] and user[1] > 18:
4 return user[2]
5
6# 好:當下就能理解
7def get_adult_user_name(user: User) -> Optional[str]:
8 if user.is_active and user.age > 18:
9 return user.name
10 return None原則二:程式碼即文件(自文件化)
程式碼本身應該說明它在做什麼:
1# 不好:需要註解才能理解
2# 檢查用戶是否有權限
3if u.r >= 3 and u.s == 'a':
4 pass
5
6# 好:程式碼本身就是說明
7if user.role_level >= ADMIN_LEVEL and user.status == UserStatus.ACTIVE:
8 pass原則三:最小意外原則
程式碼的行為應該符合讀者的預期:
1# 不好:get 通常不應該修改狀態
2def get_user_count(self):
3 self._refresh_cache() # 意外的副作用!
4 return len(self._users)
5
6# 好:get 只做讀取
7def get_user_count(self) -> int:
8 return len(self._users)
9
10def refresh_and_get_user_count(self) -> int:
11 self._refresh_cache()
12 return len(self._users)實際案例:Hook 系統重構
讓我們看一個實際的重構案例。
重構前(高認知負擔)
1def check_hook(path):
2 with open(path) as f:
3 c = f.read()
4
5 # 檢查 shebang
6 if not c.startswith("#!"):
7 return False, "no shebang"
8
9 # 解析配置
10 import yaml
11 cfg = yaml.safe_load(open(".claude/config.yaml"))
12
13 # 驗證
14 for h in cfg.get("hooks", []):
15 if h.get("path") == str(path):
16 if not os.path.exists(path):
17 return False, "not found"
18 if not os.access(path, os.X_OK):
19 return False, "not executable"
20 return True, "ok"
21
22 return False, "not registered"讀者需要:
- 記住
c是檔案內容 - 理解為什麼要檢查 shebang
- 追蹤
cfg的結構 - 理解
h和path的關係
重構後(低認知負擔)
1from lib.config_loader import load_hook_config
2from lib.hook_validator import validate_hook_file
3
4def check_hook(hook_path: Path) -> tuple[bool, str]:
5 """
6 檢查指定的 Hook 檔案是否有效。
7
8 Returns:
9 (是否有效, 訊息)
10 """
11 # 載入配置
12 config = load_hook_config()
13
14 # 檢查是否已註冊
15 if not config.is_registered(hook_path):
16 return False, "Hook 未在配置中註冊"
17
18 # 驗證檔案
19 validation_result = validate_hook_file(hook_path)
20
21 return validation_result.is_valid, validation_result.message改善之處:
- 函式名稱說明目的
- 型別提示說明輸入輸出
- 每個步驟都有清晰的意圖
- 複雜邏輯封裝在專門的函式中
自我檢查清單
閱讀或撰寫程式碼時,問自己這些問題:
- 讀者需要記住幾個變數的狀態?(應該少於 5 個)
- 讀者需要追蹤多少層呼叫?(應該少於 3 層)
- 讀者能在當下理解這段程式碼嗎?(不需要往回看)
- 變數名稱是否說明它是什麼?(不是它怎麼來的)
- 函式名稱是否說明它做什麼?(不是它怎麼做的)
小結
認知負擔是程式碼品質的終極度量標準。
所有的設計原則、最佳實踐、重構技巧,都可以用一個問題來檢驗:
這樣做是否降低了閱讀者的認知負擔?
當你面對設計決策時,不要問「這樣是否符合 DRY」或「這樣是否符合 SOLID」,而是問:
這樣寫的話,下一個讀這段程式碼的人(可能是三個月後的你自己),需要記住多少東西才能理解它?
這就是程式碼設計的核心目的。
延伸閱讀
- 命名的藝術:讓程式碼說故事 - 如何用命名降低認知負擔
- 開放封閉原則與認知負擔 - SOLID 原則的認知負擔詮釋
參考資料
- Miller, G. A. (1956). “The Magical Number Seven, Plus or Minus Two”
- Martin, R. C. (2008). “Clean Code: A Handbook of Agile Software Craftsmanship”