接手一段六個月前的程式碼,看到 processBook(),旁邊的註解寫著「處理書籍相關邏輯」。完全沒有幫助——只是重述函式名稱,沒說背後有什麼業務限制、改了會影響哪裡、當初為什麼這樣設計。

這讓我們重新思考:註解到底是為了誰而寫的?

註解的本質

程式碼註解不是程式的解釋員。它的存在是為了保護原始設計意圖,提供無法從程式碼本身推斷出來的訊息。

不應該做的:解釋程式在做什麼(好的程式碼應該自己說話)、描述函式使用方法(那是文件的工作)、充當 TODO 清單。

應該做的:作為需求保護器,防止維護時破壞原始需求;記錄設計意圖,保存業務邏輯的考量;提供維護指引,明確標示約束條件;建立程式碼與需求規格的連結。

第一原則:程式碼本身必須自說明

如果程式碼需要靠註解才能被理解,首先應該改善的是程式碼,不是補更多解釋。

process(Book book) 需要一行「檢查書籍狀態並更新進度」的註解,但如果直接命名為 updateReadingProgressWhenStatusChanges(Book book),解釋性的註解就不需要了。此時真正值得寫下來的,是這個函式背後的業務需求和約束條件:

1void updateReadingProgressWhenStatusChanges(Book book) {
2  // 需求:UC-005 閱讀進度管理
3  // 當使用者標記書籍為「閱讀中」時,自動設定進度為 0%
4  // 當使用者標記為「已完成」時,自動設定進度為 100%
5  // 約束:不可覆蓋使用者手動設定的進度值
6}

變數也一樣:final data = book.getInfo() 毫無意義,final enrichedBookMetadata = book.getMetadataWithEnrichment() 就完全自說明了。

驗證標準:移除所有註解後,如果仍能理解程式邏輯,程式碼達標。需要猜測變數含義就重新命名,無法確定函式目的就拆分函式。

第二原則:註解記錄的是需求脈絡

程式碼自說明,不代表不需要註解——需要的是不同種類的註解。

每個業務邏輯函式都應該追溯到明確的需求來源:

1/// 需求:UC-003.2 書籍分類管理
2/// 使用者可以為書籍設定多個標籤進行分類
3/// 約束:標籤名稱不可重複,最多 10 個標籤
4void addTagToBook(BookId bookId, Tag tag) {
5  // 實作...
6}

這條註解告訴維護者:這個函式的存在依據是 UC-003.2,不可以被打破的規則是標籤不可重複且最多十個。

書籍狀態的轉換順序是業務規則,不是技術細節,也應該記錄:

1/// 需求:BR-001 書籍狀態轉換規則
2/// 書籍狀態變更順序:初始 → 資訊補充中 → 資訊補充完成 → 可用
3/// 約束:不可跳過中間狀態,不可逆向轉換
4/// 例外:管理員可以直接設定為任何狀態
5BookStatus transitionBookStatus(BookStatus current, BookStatus target) {
6  // 實作...
7}

設計決策也要記錄。為什麼選懶載入加分頁?因為有效能需求:

1/// 需求:NFR-002 效能需求
2/// 書庫載入時間不可超過 2 秒
3/// 設計決策:採用懶載入 + 分頁載入策略
4/// 影響:首次載入只載入 20 本書,滾動時動態載入
5List<Book> loadLibraryWithPagination(int page, int pageSize) {
6  // 實作...
7}

一個完整的業務邏輯註解必須包含:需求來源(UC 或 BR 編號)、業務描述、約束條件、以及修改此邏輯會影響哪些功能。

第三原則:維護指引必須明確

好的維護指引讓維護者修改程式碼之前就知道會影響哪裡。不應該隨意修改的邏輯,要主動發出警告:

1/// 需求:UC-001.3 書籍唯一性檢查
2/// 同一書庫內不可有相同 ISBN 的書籍
3/// 警告:此邏輯關聯到資料一致性,修改前必須檢查:
4/// - 書籍匯入流程 (ImportBookService)
5/// - 書籍合併功能 (BookMergeService)
6/// - 資料庫索引設計 (book_isbn_unique_index)
7bool isDuplicateBook(String isbn, LibraryId libraryId) {
8  // 實作...
9}

預期會被擴展的邏輯,要提供擴展指引:

1/// 需求:UC-004 書籍搜尋功能
2/// 支援書名、作者、標籤的模糊搜尋
3/// 擴展指引:新增搜尋條件時必須:
4/// 1. 更新 SearchCriteria 值物件
5/// 2. 修改索引策略以維持效能
6/// 3. 更新搜尋測試案例涵蓋新條件
7List<Book> searchBooks(SearchCriteria criteria) {
8  // 實作...
9}

模組間的耦合關係也需要標示:

 1/// 需求:UC-006 借閱管理
 2/// 計算書籍歸還到期日
 3/// 相依性警告:此邏輯與以下模組緊密耦合
 4/// - LoanReminderService(提醒計算)
 5/// - OverdueBookDetector(逾期偵測)
 6/// - LibraryStatistics(統計計算)
 7/// 修改歸還期限計算會影響上述所有模組
 8DateTime calculateDueDate(DateTime loanDate, int loanPeriodDays) {
 9  // 實作...
10}

第四原則:結構一致的標準格式

把上面的原則整合成一個標準格式:

1/// 需求:[需求編號] [簡短描述]
2/// [詳細業務描述,說明使用者需求]
3/// 約束:[限制條件和邊界規則]
4/// [維護指引:修改須知、相依性警告、擴展要求]
5[函式簽名]

複雜業務邏輯的範例:

 1/// 需求:UC-007.1 閱讀統計分析
 2/// 計算使用者的閱讀速度和預估剩餘時間
 3/// 約束:只統計狀態為「閱讀中」的書籍,頁數必須大於 0
 4/// 計算邏輯:(已讀頁數 / 實際閱讀時間) = 閱讀速度(頁/小時)
 5/// 維護指引:修改計算公式會影響:
 6/// - 閱讀目標設定功能
 7/// - 個人化推薦系統
 8/// - 學習分析報表
 9/// 相依模組:ReadingProgressTracker, BookMetadata, UserPreferences
10ReadingSpeed calculateReadingSpeed(
11  ReadingProgress progress,
12  BookMetadata metadata,
13  Duration actualReadingTime
14) {
15  // 實作...
16}

禁止的註解模式

最常見的錯誤是重述程式碼行為。「設定書籍標題」對應的程式碼是 book.setTitle(newTitle),完全多餘。「使用 Map 快速查找避免 O(n) 複雜度」也一樣,有經驗的開發者看程式碼就知道。

UI 層特別容易出現這類問題。以 Widget 選取回饋設計為例,錯誤做法是列出技術細節:

 1/// 反例:重複描述程式碼內容
 2/// BookListItem - 書庫列表項目 Widget
 3///
 4/// 視覺設計:
 5/// - 陰影刻痕變化(凸起→凹陷)
 6/// - AnimatedContainer 200ms 過渡動畫
 7///
 8/// 觸覺回饋:
 9/// - 選擇時:HapticFeedback.selectionClick()
10/// - 取消選擇:HapticFeedback.lightImpact()
11class BookListItem extends StatelessWidget {
12  // ...
13}

這些看程式碼就能看到。真正有價值的是背後的決策依據:

 1/// 【需求來源】UC-05: 雙模式書庫展示切換 - 書籍選擇互動
 2/// 【規格文件】docs/ui_design_specification.md#book-selection-feedback
 3/// 【設計決策】採用方案C-1基礎版 - 極簡視覺回饋設計
 4/// 【為什麼選擇陰影刻痕變化】
 5/// - 不影響文字可讀性:避免背景色干擾閱讀體驗
 6/// - 符合無障礙設計:不依賴顏色作為唯一視覺提示
 7/// 【為什麼選擇差異化觸覺回饋】
 8/// - 選中 vs 取消必須有不同的觸覺回饋類型
 9/// - selectionClick 提供明確的「確認」感受
10/// - lightImpact 提供輕微的「狀態變更」提示
11/// 【修改約束】
12/// - 觸覺回饋時機不可調換(與使用者預期一致)
13/// - 陰影變化動畫時長需保持 < 250ms(符合 Material Design 規範)
14/// 【維護警告】
15/// - 此 Widget 被 3 個書庫頁面使用
16/// - 修改視覺回饋會影響整體使用者體驗一致性
17class BookListItem extends StatelessWidget {
18  // ...
19}

模糊描述也不可取——「處理書籍相關邏輯」等於沒有描述。過時的 TODO 必須清除,否則會讓人以為某個功能還沒實作。

第五原則:事件驅動架構的特殊需求

在 UseCase 或 Domain 層,函式名稱包含 handle*on*process*emit*dispatch*,或回傳類型為 Future<>Stream<> 的函式,都需要標示其在事件流中的角色:

 1/// 【需求來源】UC-01: Chrome Extension 匯入書籍資料
 2/// 【規格文件】docs/app-requirements-spec.md#chrome-extension-import
 3/// 【設計方案】方案C-1基礎版 (v0.12.7 Phase 1)
 4/// 【工作日誌】docs/work-logs/v0.12.7.md - 方案研究和設計決策
 5/// 【事件類型】BookAdded 事件處理
 6/// 【修改約束】修改時需確保事件流完整性,避免影響上游訂閱者
 7/// 【維護警告】此函式被 3 個 UseCase 依賴,修改前需檢查影響範圍
 8Future<void> handleBookAdded(BookAddedEvent event) async {
 9  // 實作...
10}

_ 開頭的私有輔助函式(_isValid*_format*_convert*_validate* 等)不包含業務邏輯,豁免詳細業務註解。

第六原則:Widget 獨立性的明確標示

非私有命名(不以 _ 開頭)、繼承自 StatefulWidgetConsumerWidgetStreamBuilderFutureBuilder 的 Widget,具備獨立狀態,需要明確記錄需求來源和修改約束:

 1/// 【需求來源】UC-05: 雙模式書庫展示切換
 2/// 【規格文件】docs/ui_design_specification.md#book-list-item
 3/// 【設計方案】方案C-1基礎版 - 陰影刻痕變化 + 觸覺回饋
 4/// 【工作日誌】docs/work-logs/v0.12.7.md - UI 互動設計
 5/// 【Widget 類型】獨立狀態管理 Widget
 6/// 【修改約束】此 Widget 具備獨立狀態,下層刷新不觸發上層重建
 7/// 【維護警告】修改前需確認子 Widget 依賴關係
 8class BookListItem extends StatefulWidget {
 9  // 實作...
10}

私有的 StatelessWidget(如 _BookTitleText_ProgressBar)和純展示型組件,只展示上層傳遞的資料,豁免詳細業務邏輯註解。

第七原則:工作日誌與規格文件的追溯鏈

設計決策涉及複雜研究或多方案比較時,幾行註解無法承載所有背景,需要建立追溯鏈:

1/// 【工作日誌】docs/work-logs/v0.12.7.md - 方案C-1基礎版設計

維護者能循著這條鏈找到完整的決策記錄。業務邏輯也可以指向規格文件章節:

1/// 【規格文件】docs/app-requirements-spec.md#section-name
2/// 【規格文件】docs/event-driven-architecture-design.md#event-flow

這讓程式碼成為整個需求、設計、實作文件體系的一部分,而不是孤立的存在。

品質驗證:兩個測試

可執行性測試:維護者看到這條註解後,能理解業務需求嗎?修改約束明確嗎?需求來源可以追溯嗎?

必要性測試:移除這條註解後,是否會遺失業務脈絡?如果移除後仍能理解程式邏輯,就要檢查它是否只是在重述程式碼。如果內容過時,直接刪除,不要保留會誤導維護者的假資訊。

結論

好的程式碼是自說明的,但好的業務系統還需要一個跨越時間的溝通機制,讓六個月後接手的人能理解每個設計決策背後的原因,不會在不了解背景的情況下破壞原始設計。

這就是我們對程式碼註解的重新定位——需求的守護者。

這是需求保護機制,不是文書工作。