命名的藝術:讓程式碼說故事
命名的藝術:讓程式碼說故事
程式碼是一個故事
好的程式碼讀起來應該像一個故事,有主角(變數)、有動作(函式)、有情節(流程)。
“someone bring something to do what”
想像你在描述一個場景:
「使用者提交表單,系統驗證資料,然後儲存到資料庫」
這就是一個故事。好的程式碼應該能像這樣被閱讀:
1# 可以像故事一樣閱讀的程式碼
2user_submitted_form = receive_form_submission(request)
3validated_data = validate_form_data(user_submitted_form)
4save_to_database(validated_data)而不是:
1# 需要解謎的程式碼
2d = get(r)
3v = check(d)
4save(v)“something transfer to some what”
另一種敘事模式是描述資料的轉換:
「原始輸入轉換成清理過的格式,再轉換成最終輸出」
1# 轉換敘事
2raw_input = read_user_input()
3cleaned_input = sanitize(raw_input)
4formatted_output = format_for_display(cleaned_input)讀者應該能像讀故事一樣理解程式碼
如果讀者需要:
- 往回翻看變數定義
- 查閱文件理解函式功能
- 猜測縮寫的含義
那就不是好的故事。好的故事讓讀者自然地跟著情節走。
變數命名的藝術
壞名稱的特徵
1# 壞:過於簡短,無法理解
2d = get_data()
3t = time.time()
4r = []
5i = 0
6
7# 壞:過於通用,無法區分
8data = get_user_data()
9data2 = get_order_data()
10temp = process(data)
11result = combine(temp, data2)
12
13# 壞:誤導性的名稱
14user_list = get_user() # 實際上回傳單一用戶,不是列表!好名稱的特徵
1# 好:說明「這是什麼」
2user_profile = get_user_profile(user_id)
3current_timestamp = time.time()
4validated_items = []
5item_index = 0
6
7# 好:能區分不同用途
8user_data = get_user_data()
9order_data = get_order_data()
10merged_report = merge_user_and_orders(user_data, order_data)
11
12# 好:名稱和實際內容一致
13active_user = get_active_user() # 單數名稱,回傳單一用戶
14active_users = get_active_users() # 複數名稱,回傳列表命名原則:說明「這是什麼」,不是「怎麼來的」
1# 不好:名稱說明來源
2config_from_yaml = load_config()
3user_after_validation = validate(user)
4
5# 好:名稱說明內容
6app_config = load_config()
7valid_user = validate(user)布林變數的命名
布林變數應該讀起來像一個問句的答案:
1# 不好:不清楚是什麼意思
2user_status = True
3file_check = False
4
5# 好:讀起來像問句的答案
6is_user_active = True # "Is user active?" - Yes
7has_valid_license = False # "Has valid license?" - No
8can_edit_document = True # "Can edit document?" - Yes
9should_retry = False # "Should retry?" - No集合類型的命名
1# 不好:不清楚是單一還是多個
2user = get_all_users() # 回傳列表,但名稱是單數
3user_list = get_user() # 回傳單一用戶,但名稱暗示列表
4
5# 好:名稱反映結構
6users = get_all_users() # 複數 = 列表
7user = get_user(user_id) # 單數 = 單一物件
8user_ids = get_all_user_ids() # 複數 + 型別提示
9user_id_to_name = build_user_map() # 說明映射關係函式命名的藝術
壞名稱的特徵
1# 壞:動詞太模糊
2def process(data):
3 pass
4
5def handle(request):
6 pass
7
8def do_something(item):
9 pass
10
11def manage_users():
12 pass
13
14# 壞:不清楚會做什麼
15def user_operation(user, action):
16 pass
17
18def data_stuff(d):
19 pass好名稱的特徵
1# 好:清楚說明動作和目標
2def validate_user_input(user_input: str) -> ValidationResult:
3 pass
4
5def extract_branch_name(git_output: str) -> str:
6 pass
7
8def format_error_message(error: Exception) -> str:
9 pass
10
11def calculate_total_price(items: list[OrderItem]) -> Decimal:
12 pass命名原則:說明「做什麼」,不是「怎麼做」
1# 不好:名稱洩漏實作細節
2def loop_through_and_sum(numbers):
3 pass
4
5def use_regex_to_find_emails(text):
6 pass
7
8# 好:名稱說明意圖
9def calculate_sum(numbers):
10 pass
11
12def extract_email_addresses(text):
13 pass常見動詞模式
| 動詞 | 使用場景 | 範例 |
|---|---|---|
get | 取得現有的值 | get_user_name() |
set | 設定值 | set_user_name() |
create | 建立新物件 | create_user() |
build | 組裝複雜物件 | build_report() |
calculate | 計算數值 | calculate_total() |
validate | 驗證資料 | validate_input() |
parse | 解析文字 | parse_config() |
format | 格式化輸出 | format_date() |
convert | 轉換型別 | convert_to_json() |
extract | 從資料中提取 | extract_ids() |
filter | 過濾資料 | filter_active_users() |
find | 尋找符合條件的 | find_user_by_email() |
is/has/can | 布林判斷 | is_valid(), has_permission() |
對稱命名
相關的函式應該有對稱的命名:
1# 好:對稱的命名
2def open_connection():
3 pass
4
5def close_connection():
6 pass
7
8# 好:對稱的命名
9def start_processing():
10 pass
11
12def stop_processing():
13 pass
14
15# 不好:不對稱
16def open_connection():
17 pass
18
19def disconnect(): # 應該是 close_connection
20 pass命名與認知負擔的關係
好的命名 = 讀者不需要記住前面發生什麼
比較這兩段程式碼:
1# 高認知負擔版本
2d = fetch()
3d = clean(d)
4d = transform(d)
5r = aggregate(d)讀到最後一行時,你需要記住:
d一開始是什麼d經過了哪些處理- 現在的
d是什麼狀態
1# 低認知負擔版本
2raw_data = fetch_user_data()
3cleaned_data = remove_invalid_entries(raw_data)
4normalized_data = normalize_formats(cleaned_data)
5report = generate_summary_report(normalized_data)讀到最後一行時,你只需要知道:
normalized_data是正規化後的資料generate_summary_report會產生報告
你不需要記住前面的處理過程,因為名稱已經告訴你每個變數「是什麼」。
認知負擔的量化分析
考慮這個問題:讀者需要追溯多少步才能理解這個變數?
1# 需要追溯 4 步
2x = get_data() # 第 1 步
3x = process(x) # 第 2 步
4x = filter(x) # 第 3 步
5result = format(x) # 第 4 步:x 到底是什麼?
6
7# 不需要追溯
8raw_data = get_data()
9processed_data = process(raw_data)
10filtered_data = filter(processed_data)
11formatted_output = format(filtered_data) # 直接看名稱就知道是過濾後的資料實際案例:Hook 系統
1# 重構前(高認知負擔)
2def check(p):
3 c = open(p).read()
4 if c.startswith("#!"):
5 l = c.split("\n")
6 if len(l) > 1:
7 return l[1].startswith("# -*- coding")
8 return False
9
10# 重構後(低認知負擔)
11def has_valid_python_header(file_path: Path) -> bool:
12 """檢查 Python 檔案是否有有效的檔頭"""
13 file_content = file_path.read_text()
14
15 has_shebang = file_content.startswith("#!")
16 if not has_shebang:
17 return False
18
19 lines = file_content.split("\n")
20 has_encoding_declaration = (
21 len(lines) > 1 and
22 lines[1].startswith("# -*- coding")
23 )
24
25 return has_encoding_declaration命名的自我檢查清單
撰寫程式碼時,對每個名稱問自己:
變數命名
- 名稱是否說明「這是什麼」?
- 讀者是否能在不看定義的情況下理解?
- 布林變數是否以 is/has/can/should 開頭?
- 集合是否使用複數形式?
- 名稱是否和實際內容一致?
函式命名
- 名稱是否以動詞開頭?
- 名稱是否說明「做什麼」而非「怎麼做」?
- 相關函式是否有對稱的命名?
- 讀者是否能從名稱推測回傳值?
- 名稱是否符合常見的動詞模式?
整體檢查
- 讀者是否能像讀故事一樣閱讀程式碼?
- 讀者是否需要往回追溯才能理解?
- 名稱是否有歧義或誤導性?
小結
命名是降低認知負擔最直接的方法。好的命名讓程式碼自己說話,不需要註解、不需要追溯、不需要猜測。
記住這個原則:
如果你需要寫註解來解釋一個變數或函式,那可能是名稱不夠好。
讓程式碼說故事,讓讀者輕鬆理解。這就是命名的藝術。
延伸閱讀
- 認知負擔:程式碼設計的核心目的 - 理解命名為何重要
- 開放封閉原則與認知負擔 - 命名在架構設計中的角色
參考資料
- Martin, R. C. (2008). “Clean Code” - Chapter 2: Meaningful Names
- Boswell, D. & Foucher, T. (2011). “The Art of Readable Code”