程式碼是一個故事

好的程式碼讀起來應該像一個故事,有主角(變數)、有動作(函式)、有情節(流程)。

“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”