本 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-001TKT-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 已能推斷 asynclatewhile 的技術動機。沒有任何業務資訊。

正例:業務情境聚焦

1/// 【需求】UC-008 列印佇列管理
2/// 印表機一次只能處理一份任務;若同時送出多份會造成頁面交錯(使用者投訴 #234)。
3/// 此函式確保所有 pending 任務依送出順序依序處理,直到佇列清空。
4/// 約束:佇列處理期間不可接受新任務(由呼叫端的鎖保證)
5/// 【相依】[PrinterLockService]
6Future<void> processPrintJobs() async { /* ... */ }

讀完可理解:業務情境(印表機獨佔)、觸發條件(多份任務同時送出)、失敗後果(頁面交錯 + 使用者投訴)、約束(外部鎖)。技術細節(async/while)由 code 自明。

3.2 Doc comment 聚焦業務情境

Doc comment 是函式、類別、介面的契約說明,應回答三個問題:

  1. 解決什麼業務問題:此程式存在的商業原因
  2. 什麼情境觸發:使用者的哪個動作、系統的哪個狀態會進入此路徑
  3. 不這樣做的產品後果:錯誤實作會造成什麼使用者可見的問題

反例:純技術描述

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 追蹤
臨時 workaroundInline 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-XXXBR-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 時應要求修改:

禁止模式反例問題替代做法
程式碼翻譯// 將計數器加 1counter++重述程式碼做什麼,違反 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'時間軸 robust5 個月後讀還看得懂嗎?依賴的 ticket / 連結還活著嗎?

Naming 子場景:四輪 review(naming-as-iterated-artifact

註解寫完該 review 一次「這段程式碼涉及的命名」。命名是 iterated artifact、第一版幾乎不對:

Frame命名專用 checklist
1第一版反映「做什麼 / 是什麼」、不是「怎麼做」;不超過 4 單字
2Grep-abilitygrep -r "<name>" 能命中目標、不被別的 entity 蓋過、不撞 framework reserved(data 等)
3Cross-call-site 一致同概念在不同 file 用同名嗎?動詞時態一致嗎?跟同 module 命名格式一致嗎?
4Impl 洩漏檢查名字含 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 各司其職

寫合格的註解是讓每個維護者(包括你自己三個月後)能在最短時間內理解業務意圖、避免破壞設計、安全地擴展功能。