Writing Code Comments — 程式碼註解撰寫指引
本 reference 提供撰寫程式碼註解的完整指引,整合核心寫作原則在註解情境的具體應用。讀者只需讀本文件,即可獨立寫出合格的註解,不需再讀其他 reference。
核心命題:註解是業務需求和設計意圖的守護者。註解的認知依賴必須跟著程式依賴一起降低——介面的註解只寫契約,不洩漏實作;實作的註解說明業務規則,不描述語法選擇。
適用時機
當你準備寫 / 改 / 審查以下任一註解時,應用本指引:
| 情境 | 說明 |
|---|---|
Doc comment(///、/** */、"""...""") | 位於函式、類別、介面、模組宣告前方,供外部閱讀者和工具消費 |
Inline comment(//、#) | 位於實作內部,說明「為什麼這樣做」 |
| 模組 README 或檔案頂部 header | 描述此模組解決的問題 |
| 介面 / 抽象類別的 docstring | 定義契約(做什麼、輸入輸出語意、使用情境) |
不適用:commit message、PR description、ticket context(另見其他情境 reference)。
原則一:原子化 × 註解
一則註解只解釋一個概念。 一個函式若需要註解說明多個不相關的關注點,應先拆分函式,再為每個函式寫獨立註解。
正例
1/// 【需求】UC-003 書籍狀態轉換
2/// 書籍從「閱讀中」改為「已完成」時,自動將進度設為 100%
3/// 約束:不可覆蓋使用者手動設定的進度值
4void markBookCompleted(BookId id) { /* ... */ }
5
6/// 【需求】UC-003 書籍狀態轉換
7/// 書籍從「未開始」改為「閱讀中」時,初始化進度為 0%
8/// 約束:不重置已有的閱讀時間紀錄
9void markBookReading(BookId id) { /* ... */ }每則註解只描述一個狀態轉換規則,閱讀者不需同時記住兩條規則才能理解一個函式。
反例
1/// 【需求】UC-003 書籍狀態轉換
2/// 處理所有狀態變化:未開始→閱讀中時進度設 0%、閱讀中→完成時進度設 100%、
3/// 完成→未開始時清空進度、任何狀態→暫停時保留進度並記錄暫停時間
4/// 約束:不覆蓋使用者手動進度,除了清空場景
5/// 例外:管理員可跳過轉換規則直接設定任何狀態
6void handleBookStatusChange(BookId id, Status from, Status to) { /* ... */ }註解承擔 4 種狀態轉換的全部規則,閱讀者認知負擔爆表(規則數 × 例外數 = 複合爆炸),函式本身也違反單一職責。
判斷標準
- 註解超過 5 行且包含 2+ 個「且」「或」「除了」「同時」?→ 拆分函式
- 同一註解內有多個無關的約束條件?→ 拆分函式
原則二:索引 × 註解
需求編號(UC-XXX、BR-XXX、TKT-ID)是註解的「連結卡」,讓註解與規格文件、ticket、worklog 之間建立可追溯的索引。
正例
1/// 【需求】UC-004 書籍搜尋功能
2/// 【規格】docs/spec/search.md#fuzzy-match
3/// 支援書名、作者、標籤的模糊搜尋
4/// 擴展指引:新增搜尋條件時必須更新 SearchCriteria 值物件
5List<Book> searchBooks(SearchCriteria criteria) { /* ... */ }閱讀者可從 UC-004 跳到需求文件,從規格路徑跳到設計細節。三者互為索引卡片。
反例
1/// 實作書籍搜尋功能,根據某些條件查找
2List<Book> searchBooks(SearchCriteria criteria) { /* ... */ }沒有需求索引,閱讀者無法回溯「這個函式為什麼存在」「規格在哪」。
索引內容建議
| 索引類型 | 格式 | 用途 |
|---|---|---|
| 需求來源 | 【需求】UC-003 或 【需求】BR-007 | 連回需求規格 |
| 規格文件 | 【規格】docs/spec/xxx.md#section | 連回設計細節 |
| 相依模組 | 【相依】[ModuleA], [ModuleB] | 用命名引用,不解釋實作 |
禁止:註解內放 ticket ID(如 v1.2.3-W5-001、TKT-1234),ticket 是臨時追蹤編號,會被搬移歸檔;需求編號(UC/BR)才是穩定索引。
原則三:意圖顯性與商業邏輯 × 註解
Doc comment 描述業務情境,不解釋語法選擇。 閱讀者想知道「這段程式解決什麼業務問題」「什麼情境下會觸發」「如果不這樣做會發生什麼產品層面後果」;語法細節(while vs if、async、late 變數)讀者看 code 就能推斷,不需要 doc comment 佔用最靠近視線的位置解釋。
3.1 語法 vs 業務情境區分
註解的語法層 vs 業務層區分:
| 層級 | 註解回答的問題 | 範例 |
|---|---|---|
| 語法層(禁止在 doc comment) | 為什麼用 while 而不是 if?為什麼 late?為什麼 async? | 讀 code 可推斷,寫在 doc 浪費視線位置 |
| 業務層(doc comment 聚焦) | 此程式解決什麼業務問題?什麼情境觸發?不這樣做會發生什麼產品後果? | 「印表機鎖定時,後續任務必須排隊等待;若跳過鎖定會導致列印頁面交錯」 |
反例:語法選擇解釋
1/// 使用 while 迴圈而非 if,因為 job queue 可能有多筆任務需要依序處理
2/// 使用 async 以避免阻塞 UI thread
3/// 使用 late 變數因為 printer 物件在建構時尚未就緒
4Future<void> processPrintJobs() async {
5 while (jobs.isNotEmpty) { /* ... */ }
6}這三行都在解釋語法選擇,讀者看 code 已能推斷 async、late、while 的技術動機。沒有任何業務資訊。
正例:業務情境聚焦
1/// 【需求】UC-008 列印佇列管理
2/// 印表機一次只能處理一份任務;若同時送出多份會造成頁面交錯(使用者投訴 #234)。
3/// 此函式確保所有 pending 任務依送出順序依序處理,直到佇列清空。
4/// 約束:佇列處理期間不可接受新任務(由呼叫端的鎖保證)
5/// 【相依】[PrinterLockService]
6Future<void> processPrintJobs() async { /* ... */ }讀完可理解:業務情境(印表機獨佔)、觸發條件(多份任務同時送出)、失敗後果(頁面交錯 + 使用者投訴)、約束(外部鎖)。技術細節(async/while)由 code 自明。
3.2 Doc comment 聚焦業務情境
Doc comment 是函式、類別、介面的契約說明,應回答三個問題:
- 解決什麼業務問題:此程式存在的商業原因
- 什麼情境觸發:使用者的哪個動作、系統的哪個狀態會進入此路徑
- 不這樣做的產品後果:錯誤實作會造成什麼使用者可見的問題
反例:純技術描述
1/// 將 Book 物件序列化為 JSON 字串並寫入 SharedPreferences
2/// 使用 jsonEncode 和 keyPrefix = 'book_'
3void saveBook(Book book) { /* ... */ }只描述「怎麼做」,沒說「為什麼做」「什麼時候該做」。
正例:業務情境驅動
1/// 【需求】UC-002 離線書庫
2/// 使用者關閉 app 後重開仍能看到書庫清單(不需重新從 Readmoo 抓取)。
3/// 觸發時機:使用者匯入新書、編輯書籍標籤、標記進度時。
4/// 若未持久化,使用者每次開 app 都要等網路載入,離線時完全無法使用。
5void saveBook(Book book) { /* ... */ }3.3 禁止 Doc comment 寫 TODO / placeholder
Doc comment 描述穩定契約,不應包含「等等要做 X」「暫時這樣」「未來會改」等臨時性內容。TODO/placeholder 應該:
| 內容 | 放哪裡 |
|---|---|
| 未完成工作 | Ticket 或 inline comment(// TODO: ...) |
| 暫時實作說明 | Inline comment,並建立 ticket 追蹤 |
| 臨時 workaround | Inline comment 指向 issue link |
| 穩定契約 | Doc comment |
反例
1/// 【需求】UC-005 閱讀進度同步
2/// TODO: 之後要加入衝突解決邏輯
3/// 暫時實作:直接覆蓋本地值,不處理同時間多裝置修改
4/// 注意:此函式還沒寫單元測試(placeholder)
5Future<void> syncProgress(Book book) { /* ... */ }Doc comment 是契約,卻充滿「之後」「暫時」「還沒」——讀者無法判斷哪些是契約、哪些是待辦。
正例
1/// 【需求】UC-005 閱讀進度同步
2/// 將本地進度寫入雲端。同時間多裝置修改時,採 last-write-wins。
3/// 約束:網路失敗時本地變更會留在 outbox 等待重試(呼叫端不需處理失敗)
4Future<void> syncProgress(Book book) { /* ... */ }
5
6// TODO(<ticket-id>): last-write-wins 是暫時策略,實作 vector clock 衝突解決
7// 暫時不寫測試,<ticket-id> 追蹤Doc 只描述當前契約(LWW 策略是當前的契約,不是「暫時」),TODO 放 inline 並指向 ticket。
3.4 註解貼合抽象層級
介面的註解只寫契約,不洩漏實作。 程式刻意解耦(介面 vs 實作、模組 vs 模組)是為了讓讀/改某一層時不需通盤理解其他層;若註解又把實作細節拉回介面裡,讀介面等於要先認識實作,抽象帶來的好處被註解抵消掉。
核心原則:註解的認知依賴必須跟著程式依賴一起降低。
反例:介面 doc 洩漏實作
1abstract class OrderRepository {
2 /// 取得訂單清單。
3 /// 資料來源由實作層決定(目前為定時輪詢 [IOnlineOrderService]),
4 /// 消費端不需關心來源為輪詢或推播。
5 Future<List<Order>> fetchOrders();
6}既說「不需關心」又主動告知「目前是輪詢」,自相矛盾且洩漏實作。讀者看完反而被迫知道實作是輪詢,介面的抽象價值被註解抵消。
正例:只寫契約 + 命名引用
1abstract class OrderRepository {
2 /// 取得訂單清單。
3 /// 保證按建立時間由新到舊排序。空結果回傳空陣列(不拋例外)。
4 /// 資料來源:[IOnlineOrderService]
5 Future<List<Order>> fetchOrders();
6}讀者想知道資料來源細節可跳轉到 IOnlineOrderService;不想知道可跳過。註解的認知依賴和程式的依賴方向一致(介面 → 服務命名引用,不反向揭露實作)。
同理適用範圍
| 位置 | 只寫什麼 | 不寫什麼 |
|---|---|---|
| 介面 doc | 契約(做什麼、輸入輸出語意、使用情境) | 「目前實作用什麼方式」 |
| 模組 README | 此模組解決的問題、對外 API | 內部類別結構、用了什麼演算法 |
| 函式 docstring | 解決什麼問題、輸入輸出約定 | 內部怎麼解決(除非行為細節是契約,如冪等性、順序保證) |
| 抽象類別 doc | 子類需實作的契約、行為承諾 | 某個子類的具體實作方式 |
例外:當行為細節本身就是契約的一部分時(例如「保證冪等」「保證依序處理」「保證不阻塞」),這些細節就是契約,必須寫在 doc 中。關鍵判斷:消費端會因此細節而改變使用方式嗎?是 → 寫進 doc;否 → 留給實作層。
原則四:可查詢性 × 註解
註解的關鍵字設計讓 grep / AI 能定位。 當維護者搜尋「哪個函式處理書籍狀態轉換」時,註解應包含讓搜尋命中的詞彙。
關鍵字設計建議
| 類型 | 格式 | 範例 |
|---|---|---|
| 需求索引 | 【需求】UC-XXX | 【需求】UC-003 書籍狀態轉換 |
| 業務概念 | 使用業務詞彙而非技術詞彙 | 用「狀態轉換」而不只寫「update」 |
| 分類標記 | 【事件】、【約束】、【相依】 | 【事件】BookAddedEvent 處理 |
| 警告標記 | 【警告】、【維護】 | 【維護】修改前檢查 3 個呼叫端 |
正例
1/// 【需求】UC-006 借閱到期提醒
2/// 【事件】ReminderTriggeredEvent 消費端
3/// 當借閱書籍距離到期日 < 3 天時觸發提醒。
4/// 【約束】同一本書 24 小時內只提醒一次(由 ReminderDebouncer 處理)
5/// 【維護】修改提醒閾值需同步更新 ReminderDebouncer.windowHours
6Future<void> sendDueReminder(Loan loan) { /* ... */ }搜尋「到期提醒」、「ReminderTriggeredEvent」、「ReminderDebouncer」都能命中;【事件】 標記讓過濾所有事件消費端變可能。
反例
1/// 檢查並處理到期相關的事情
2Future<void> sendDueReminder(Loan loan) { /* ... */ }「相關的事情」是語意黑洞,grep 搜尋業務詞彙無法命中。
分隔符規範
使用一致的分隔符讓結構可被 regex 解析:
【XXX】全形方括號:語義標記(需求、事件、約束、維護、相依)[ModuleName]半形方括號:類別 / 模組命名引用(可被 LSP 跳轉)UC-XXX、BR-XXX、#section:索引格式固定,便於全專案統一搜尋
原則五:欄位設計 × 註解(不同抽象層的註解寫法)
不同抽象層的註解寫法不同——這是「欄位設計精神」在註解的應用:同一概念在不同位置用不同角度描述,避免重複。
抽象層級對照表
| 層級 | 註解角度 | 回答的問題 | 範例重點 |
|---|---|---|---|
| 介面 / 抽象類別 | 契約視角 | 做什麼?承諾什麼? | 「保證按建立時間排序」 |
| 實作類別 | 策略視角 | 用什麼策略達成契約?(策略選擇理由,非語法) | 「用資料庫 ORDER BY 而非記憶體排序,因為訂單數可能 > 10 萬」 |
| 函式 | 步驟視角 | 此函式在整個流程扮演什麼角色? | 「在轉帳流程中負責扣款步驟」 |
| Inline(行內) | 決策視角 | 這行為何必要?不這樣會怎樣? | // 必須先 flush cache,否則下一行的 read 會拿到舊值 |
正例:同一業務概念的多層寫法
1// 介面層:只寫契約
2abstract class OrderSearcher {
3 /// 【需求】UC-004 訂單查詢
4 /// 依關鍵字搜尋訂單。結果按建立時間新到舊排序。
5 /// 無結果回傳空陣列(不拋例外)。
6 Future<List<Order>> searchByKeyword(String keyword);
7}
8
9// 實作層:解釋策略選擇
10class DbOrderSearcher implements OrderSearcher {
11 /// 【需求】UC-004 訂單查詢
12 /// 使用資料庫全文索引(非記憶體 filter),因訂單量可能達十萬級。
13 /// 【相依】[FullTextIndex]
14 @override
15 Future<List<Order>> searchByKeyword(String keyword) async {
16 // ... 實作
17 // 先 normalize 輸入(小寫 + 移除標點),否則索引無法命中
18 final normalized = _normalize(keyword);
19 // ... 查詢
20 }
21}- 介面註解:讀者想知道契約即足夠
- 實作註解:讀者想知道「為什麼用 DB 不用記憶體」
- Inline 註解:讀者想知道「這行 normalize 為什麼不可刪」
三處註解從不同角度描述同一業務概念,不重複。
反例:三層寫相同內容
1abstract class OrderSearcher {
2 /// 使用資料庫全文索引搜尋訂單,依建立時間排序
3 Future<List<Order>> searchByKeyword(String keyword);
4}
5
6class DbOrderSearcher implements OrderSearcher {
7 /// 使用資料庫全文索引搜尋訂單,依建立時間排序
8 Future<List<Order>> searchByKeyword(String keyword) async {
9 // 使用資料庫全文索引搜尋訂單,依建立時間排序
10 // ...
11 }
12}介面洩漏實作(違反原則 3.4)、三層內容重複(違反 DRY)、實作沒解釋策略選擇(讀者看不到「為什麼用 DB」)。
禁止模式清單
以下註解模式禁止使用,CR 時應要求修改:
| 禁止模式 | 反例 | 問題 | 替代做法 |
|---|---|---|---|
| 程式碼翻譯 | // 將計數器加 1 → counter++ | 重述程式碼做什麼,違反 DRY | 刪除註解或改寫為「為什麼加 1」 |
| Doc 寫 TODO | /// TODO: 之後加驗證 | 契約文件混入待辦 | 移到 inline // TODO(TKT-X): ... |
| Doc 寫 placeholder | /// 暫時先這樣做 | 契約不應是臨時的 | 寫成當前的真實契約,待辦另外追蹤 |
| 語法選擇解釋 | // 用 while 因為要處理多筆 | 讀 code 可推斷,浪費視線位置 | 改寫為業務情境(為什麼有多筆) |
| 技術實作描述 | // 使用 Map 快速查找 | 讀 code 可推斷 | 若效能是契約,寫「保證 O(1) 查詢」 |
| 模糊業務描述 | // 處理書籍相關邏輯 | 語意黑洞,grep 無法命中 | 具體描述觸發情境和規則 |
| 過時 TODO | // TODO: 加驗證(但驗證已完成) | 誤導維護者 | 刪除 |
| 介面洩漏實作 | 介面 doc 寫「目前用輪詢」 | 破壞抽象,認知依賴爆增 | 改為命名引用 + 只寫契約 |
| 無索引業務邏輯 | 純技術描述,無 UC/BR 編號 | 無法回溯需求 | 加 【需求】UC-XXX |
| 冗長混合責任 | 一則註解含 4 種狀態轉換規則 | 認知負擔爆表 | 拆函式 + 拆註解 |
自檢清單(寫完註解問自己)
- 這則註解描述的是業務問題還是語法選擇?(若是後者,刪除或改寫)
- 若這是 doc comment,讀者不看實作能理解契約嗎?
- 若這是介面註解,有洩漏任何「目前實作用什麼」嗎?
- 註解包含 TODO / placeholder / 暫時 嗎?(若是,移到 inline + ticket)
- 一則註解只解釋一個概念嗎?(若不是,拆函式)
- 有需求索引(UC/BR)讓閱讀者能回溯嗎?
- 關鍵字設計讓 grep / AI 能找到嗎?(業務詞彙而非通用詞彙)
- 註解貼合所在抽象層嗎?(介面寫契約、實作寫策略、inline 寫決策)
多輪 Re-read Pass(multi-pass refinement)
寫完上方自檢還不是 done — 自檢是「同 frame 的最後一掃」、不是 multi-pass。Multi-pass 要求每輪用不同 frame catch 不同層的錯(literal-interception-vs-behavioral-refinement / writing-multi-pass-review)。
註解用的核心三輪 + 兩輪程式碼專屬:
| 輪 | Frame | 程式碼註解專用 checklist |
|---|---|---|
| 1 | 生成 | 把「為什麼」寫出來、預期語句不順 |
| 2 | 對意圖(ease-of-writing-vs-intent-alignment) | 寫的是「為什麼這樣做」嗎、不是「程式在做什麼」?業務需求 vs 語法選擇分清楚? |
| 3 | 機會成本語氣 | 「必須」「不可」翻成「在 X 情境下選擇 A 因為 ⋯⋯」 |
| 4' | 介面 vs 實作分層 | doc comment 不洩漏 impl、inline comment 講 why 不講 what、抽象層對齊嗎? |
| 5' | 時間軸 robust | 5 個月後讀還看得懂嗎?依賴的 ticket / 連結還活著嗎? |
Naming 子場景:四輪 review(naming-as-iterated-artifact)
註解寫完該 review 一次「這段程式碼涉及的命名」。命名是 iterated artifact、第一版幾乎不對:
| 輪 | Frame | 命名專用 checklist |
|---|---|---|
| 1 | 第一版 | 反映「做什麼 / 是什麼」、不是「怎麼做」;不超過 4 單字 |
| 2 | Grep-ability | grep -r "<name>" 能命中目標、不被別的 entity 蓋過、不撞 framework reserved(data 等) |
| 3 | Cross-call-site 一致 | 同概念在不同 file 用同名嗎?動詞時態一致嗎?跟同 module 命名格式一致嗎? |
| 4 | Impl 洩漏檢查 | 名字含 impl 細節嗎(fetchUserViaSql)?換 impl 後名字還對嗎?data structure 細節洩漏嗎? |
跳輪規則:
- Loop counter / close-up var:跑輪 1
- Test code 內部 helper:1 + 4
- 跨 team API / DB schema:每輪都跑、跑兩遍
- Production-facing URL / endpoint:不可跳
詳見 naming-as-iterated-artifact。
常見意外場景
場景 1:改 bug 時順手改壞註解
修 bug 改了實作,忘記檢查 doc 是否仍成立。SOP:改實作前先讀 doc comment;改完後回檢「doc 描述是否仍準確」。若契約變了,doc 必須同步修。
場景 2:抄別的函式的 doc comment
抄來的 doc 描述可能來自另一個業務情境。SOP:抄完後必須重寫需求索引和業務描述,確認「這個函式解決的問題」與原函式相同才能保留 doc。
場景 3:用 AI 產生 doc comment
AI 傾向產生「語法翻譯式」註解(使用 while 因為…)。SOP:AI 產出後必須人工審查,把「語法理由」全刪,補「業務理由」。
場景 4:介面加新方法時 copy 實作的 doc
實作的 doc 可能包含策略細節,直接 copy 到介面會洩漏實作。SOP:新增介面方法時,只保留契約描述部分(做什麼、輸入輸出、使用情境),刪除策略細節(用什麼演算法、為什麼用 DB)。
回顧總結
| 原則 | 在註解情境的具體意義 |
|---|---|
| 原子化 | 一則註解只解釋一個概念;函式需多條規則就拆函式 |
| 索引 | 用 UC/BR 編號建立註解 → 需求 → 規格的追溯鏈 |
| 意圖顯性與商業邏輯 | Doc 聚焦業務情境;區分語法 vs 業務;不寫 TODO/placeholder;貼合抽象層級 |
| 可查詢性 | 使用業務詞彙、語義標記(【XXX】)、命名引用([ModuleName]) |
| 欄位設計 | 不同抽象層用不同角度寫註解,介面/實作/函式/inline 各司其職 |
寫合格的註解是讓每個維護者(包括你自己三個月後)能在最短時間內理解業務意圖、避免破壞設計、安全地擴展功能。