iT邦幫忙

2026 iThome 鐵人賽

DAY 17
1

甚麼時候需要撰寫註解?

前一章已經透過函式拆分、專用資料結構與明確命名,讓程式本身表達流程與功能意圖。結構清楚之後,仍有部分資訊無法直接寫進函式名稱或條件判斷,例如選擇某種做法的原因、功能規則的確認來源,以及看似多餘卻暫時不能移除的相容處理。

註解適合保存這些背景。它不需要逐行翻譯程式,也不應該替過長或命名含糊的實作補充說明。判斷是否需要註解時,可以先問兩個問題:這項資訊能否透過程式結構清楚表達,以及缺少它是否會讓後續修改產生錯誤判斷。

本章會說明哪些內容適合寫入註解、哪些問題應該先修改程式,以及如何讓註解隨實作一起維護。

先讓程式碼表達可以直接看出的內容

自我說明程式碼(Self-Documenting Code)透過清楚命名、適當函式大小與明確資料結構表達意圖。讀者可以從程式看出輸入如何處理、條件如何判斷及結果如何產生,不需要依賴另一段文字重述相同動作。

例如,下列註解只把條件判斷再說一次:

// 檢查匯入數量是否有效
if (amount >= 0) {
    acceptImportRow(row)
}

如果「有效」確實是一項功能概念,可以改用名稱表達:

if (isValidImportAmount(amount)) {
    acceptImportRow(row)
}

新的名稱讓呼叫端直接說明判斷目的,規則細節則集中在 isValidImportAmount。日後條件改變時,只需要修改函式與測試,不會留下與實作不一致的逐行說明。

自我說明程式碼仍有表達限制。它能顯示目前採用哪項條件,通常無法說明條件的確認來源,也不適合把一段決策背景塞進函式名稱。這些資訊才是註解應該補充的內容。

用註解補充選擇背後的原因

有價值的註解會回答「為甚麼採用這個做法」。它可以記錄系統限制、被排除方案、必要的處理順序,或某項看似可以簡化的程式為何需要保留。

例如,Math.trunc 已經清楚顯示程式會捨去小數。註解應該補充採用這項轉換的原因:

function toExternalAmount(value) {
    // 目標格式只接受整數,因此在輸出前捨去小數。
    return Math.trunc(value)
}

註解中的「目標格式只接受整數」無法從 Math.trunc 直接得知。如果這項限制改變,開發人員也能知道轉換方式需要一起檢查。

下列資訊通常適合用註解補充:

資訊類型 註解需要記錄的內容
設計原因 為何選擇目前做法,以及哪些限制影響決定。
判斷順序 順序改變會造成甚麼功能差異。
特殊邊界 看似可以合併的條件為何需要分開處理。
暫時限制 限制目前為何存在,以及解除後要調整哪些程式。
外部格式 程式必須符合的欄位、精度、順序或相容條件。

註解應該保留完成判斷所需的最少資訊。背景需要長篇說明時,可以在註解中留下可追溯的文件或問題識別,再把完整內容放在適合維護的位置。

記錄功能規則與限制的確認來源

程式可以顯示目前如何處理輸入,卻不一定能說明這項行為來自已確認的需求、格式規範、歷史相容條件,或只是早期實作留下的結果。當來源會影響規則能否修改時,註解應該留下可確認的參考。

例如,空白代碼究竟要拒絕還是套用預設值,無法只靠條件判斷得知決策背景:

// RULE-IMPORT-07:空白代碼要保留為拒絕結果,不套用預設值。
const rejection = code === '' ? 'code_required' : null

RULE-IMPORT-07 可以對應需求紀錄、格式規格或其他可持續取得的決定。註解不必複製整份文件,只要讓後續修改者能確認規則內容、適用範圍與目前版本。

參考來源失效時,註解也會失去用途。使用檔案路徑、段落識別或問題編號時,要選擇能隨專案保存或持續查詢的形式。只有人名、日期或「以前討論過」等資訊,通常不足以還原決定內容。

為相容處理留下移除條件

目標系統在替換期間可能需要支援既有資料格式、互動方式或相依項目版本。相容程式看起來可能重複、繞路或違反目前慣例,如果沒有說明,後續修改者可能誤以為它是多餘程式而直接刪除。

相容註解至少要說明支援對象、保留原因、移除條件與追蹤位置:

function parseSourceDate(value) {
    // 暫時支援來源格式 v1;所有來源改用 v2 後移除。追蹤:ISSUE-184。
    return normalizeLegacyDate(value)
}

「暫時保留」沒有提供可執行的後續判斷。上例明確指出格式 v1 是相容對象,完成 v2 切換是移除條件,ISSUE-184 則連回追蹤紀錄。條件達成後,應該刪除相容程式與註解,不能讓已失效的說明繼續留在目標系統。

如果相容處理會在特定日期失效,仍要記錄實際完成條件。只有日期可能在工作尚未完成時經過,也無法說明移除前需要確認哪些輸入、資料或相依項目。

說明不直觀的取捨與邊界

某些演算法、效能處理、安全性限制或邊界條件具有刻意安排。程式能顯示執行順序,卻不一定能說明順序背後的取捨。這類註解應該指出改動可能造成的具體影響。

例如,排序可能用來產生可穩定比較的輸出,與顯示需求無關:

// 固定輸出順序,讓相同輸入可以直接進行完整結果比較。
items.sort((left, right) => left.id - right.id)

如果只寫「依識別資料排序」,註解仍然只是重述程式。補上穩定比較的目的後,後續修改者就能判斷是否可以更換排序欄位或移除排序。

註解不能取代測試。必要順序、特殊邊界與安全性限制仍應該建立相應案例,註解負責說明原因與影響,測試負責確認程式持續符合條件。

使用文件註解描述公開契約

文件註解(Documentation Comment)用來說明公開函式、類別或其他對外使用介面的契約。內容可以包含用途、輸入限制、回傳結果、可能拋出的錯誤及副作用,讓呼叫端不需要閱讀內部實作就能正確使用。

JavaScript 可以使用 JSDoc 記錄這些資訊:

/**
 * 將數字文字解析成非負安全整數。
 * @param {string} value - 只包含十進位數字的文字。
 * @returns {number} 解析後的非負安全整數。
 * @throws {TypeError} 輸入格式錯誤或超出安全整數範圍時拋出。
 */
export function parseCount(value) {
    const count = Number(value)
    if (typeof value !== 'string' || !/^\d+$/.test(value) || !Number.isSafeInteger(count)) {
        throw new TypeError('value 必須是非負安全整數文字')
    }
    return count
}

這段文件註解描述 parseCount 的輸入格式、成功結果與失敗行為,實作也確實符合三項內容。如果函式可能修改呼叫端傳入的資料、保存狀態或產生其他外部影響,也應該在契約中說明。

私有函式不需要一律加入完整文件註解。名稱、參數與回傳值已經足以表達用途時,額外標註只會增加維護內容。公開程度、使用範圍及錯誤使用的影響,才是決定文件註解詳細程度的依據。

移除只重述程式碼的註解

逐行敘述程式動作會增加閱讀量,卻沒有提供新資訊。當實作變更而註解沒有同步修改時,這類說明還可能讓讀者相信過時行為。

以下註解可以直接刪除:

// 將重試次數加一
retryCount += 1

下列註解也沒有補充判斷目的:

// 如果狀態等於 completed
if (status === 'completed') {
    publishResult(result)
}

如果條件的功能意義不清楚,應該優先調整名稱或抽取函式。例如,isReadyToPublish(status) 比逐字描述比較動作更容易表達目的。如果條件背後還有無法從名稱得知的限制,再另外補充原因。

註解過長時先改善程式結構

一段註解如果需要逐步說明變數如何改變、各分支如何執行及最後如何組合結果,通常表示程式結構仍不夠清楚。前一章介紹的重新命名、縮小作用域、抽取函式與拆分責任,應該先用來降低理解成本。

例如,複雜的重試條件可以先抽成能表達目的的函式:

if (shouldRetry(error, attemptCount)) {
    await retryOperation()
}

shouldRetry 應該由測試說明各種錯誤與次數的判斷結果。如果最大次數或特定錯誤限制來自外部規範,則可以在規則實作旁留下來源,不需要在呼叫端用長篇註解解釋所有分支。

抽取後如果仍需要大量文字才能說明輸入與結果,可能表示函式同時負責多項工作,或使用的資料結構缺少明確狀態。此時應該繼續檢查程式邊界,避免讓註解成為難懂實作的固定補丁。

不在註解中保留停用程式碼

把停用程式碼改成註解會讓原始碼同時存在目前實作與歷史片段。讀者無法判斷這段內容是否仍有用途、是否可以恢復,以及它與目前功能規則有何關係。

已刪除內容應該由 Git 歷史保存。需要查找時,可以從提交紀錄確認刪除原因、當時版本與相關修改。這種方式比在原始碼中保留無法執行的片段更容易追溯,也不會干擾目前程式。

如果某段程式預計之後恢復,應該記錄恢復條件與追蹤項目,並將實作保存在適合的版本紀錄中。無法說明恢復條件的停用片段,不應該因為「可能還會用到」而長期留在註解內。

讓 TODO 與 FIXME 可以追蹤

TODOFIXME 可以提醒尚未完成的工作或已知問題,但單獨留下標記無法說明原因、影響與完成方式。專案應該先定義兩者用途,並且要求每項註記能連回持續維護的追蹤紀錄。

標記 建議用途 至少需要記錄的內容
TODO 已確認需要完成,但目前尚未納入本次修改的工作。 保留原因、完成或移除條件、追蹤項目。
FIXME 已確認目前實作會產生錯誤或不完整結果。 問題情境、預期修正結果、追蹤項目。

註記可以維持一行,但內容要能支持後續處理:

// TODO(ISSUE-184):來源格式 v1 停用後,移除相容轉換。
// FIXME(ISSUE-207):空白代碼會被接受;修正後必須通過案例 IMP-012。

只有「之後處理」、姓名或建立日期的註記,無法判斷工作是否仍需要進行。追蹤項目完成、需求改變或相關程式刪除時,也要同步移除註記。

TODOFIXME 不應該取代目前修改範圍內必要的錯誤處理。如果問題會讓本次功能無法符合驗收條件,就要先修正或明確阻止功能進入下一階段,不能只留下註記後繼續執行。

將註解視為程式的一部分

註解會影響讀者如何理解與修改程式,因此需要和實作一起檢查。功能規則、外部格式、相依項目或錯誤行為變更時,要同步更新相關註解與文件註解。

程式修改完成後,可以使用下列問題檢查:

  • 註解是否提供程式無法直接表達的原因、來源或限制?
  • 是否能先透過命名、函式拆分或資料結構改善可讀性?
  • 功能規則是否連回可持續取得的確認來源?
  • 相容處理是否說明支援對象、移除條件與追蹤位置?
  • 不直觀的取捨是否說明改動後可能產生的具體影響?
  • 文件註解描述的輸入、輸出、錯誤與副作用是否符合目前實作?
  • 是否已經移除逐行重述程式與停用程式碼的註解?
  • TODOFIXME 是否具有原因、完成條件與追蹤方式?
  • 已經失效的註解是否和相關程式一起更新或刪除?

程式審查也要檢查註解是否正確,不能只確認語法與執行結果。錯誤的註解可能比沒有註解更難發現,因為它會提供看似合理但已經過時的解釋。

重點整理

  • 程式能透過命名、函式拆分與資料結構直接表達的內容,應該優先改善程式,不需要再用註解逐行敘述。
  • 註解適合記錄設計原因、判斷順序、特殊邊界、功能規則來源及外部格式限制。
  • 相容處理的註解要說明支援對象、保留原因、移除條件與追蹤位置,條件達成後要連同程式一起刪除。
  • 不直觀的演算法、效能處理與安全性限制需要說明取捨及改動影響,相關行為仍要由測試保護。
  • 文件註解應該描述公開介面的用途、輸入、回傳結果、錯誤與副作用,並且和目前實作保持一致。
  • 只重述程式、解釋混亂結構或保留停用程式碼的註解應該移除,必要資訊改由命名、重構與 Git 歷史保存。
  • TODOFIXME 要記錄原因、完成條件及追蹤方式,不能取代目前範圍內必要的修正與錯誤處理。
  • 註解是程式的一部分,功能與限制改變時要同步更新或刪除。

上一篇
[Day 16] 如何拆分大型流程?
系列文
遠古聖遺物改造工程:遺留系統全面重構實務指南17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言