程式碼註解撰寫方法論
接手一段六個月前的程式碼,看到 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 獨立性的明確標示
非私有命名(不以 _ 開頭)、繼承自 StatefulWidget、ConsumerWidget、StreamBuilder 或 FutureBuilder 的 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這讓程式碼成為整個需求、設計、實作文件體系的一部分,而不是孤立的存在。
品質驗證:兩個測試
可執行性測試:維護者看到這條註解後,能理解業務需求嗎?修改約束明確嗎?需求來源可以追溯嗎?
必要性測試:移除這條註解後,是否會遺失業務脈絡?如果移除後仍能理解程式邏輯,就要檢查它是否只是在重述程式碼。如果內容過時,直接刪除,不要保留會誤導維護者的假資訊。
結論
好的程式碼是自說明的,但好的業務系統還需要一個跨越時間的溝通機制,讓六個月後接手的人能理解每個設計決策背後的原因,不會在不了解背景的情況下破壞原始設計。
這就是我們對程式碼註解的重新定位——需求的守護者。
這是需求保護機制,不是文書工作。