計文檔怎么寫:用接口契約與狀態(tài)機把“詳細(xì)”落到實處)
簡介模塊設(shè)計文檔是軟件開發(fā)中用于明確系統(tǒng)結(jié)構(gòu)、模塊職責(zé)與協(xié)作方式的關(guān)鍵產(chǎn)物。這份模板面向項目經(jīng)理、系統(tǒng)架構(gòu)師、開發(fā)與測試人員適合在項目初期統(tǒng)一設(shè)計溝通語言有效規(guī)避模塊邊界模糊、接口依賴混亂等問題。壓縮包內(nèi)僅一個doc格式文件容量約93KB輕量且便于直接改編為實際項目的文檔底稿。文檔章節(jié)完整且可操作覆蓋文檔目的與范圍、讀者對象、參考文獻(xiàn)、術(shù)語與縮寫解釋、模塊命名規(guī)則、模塊匯總表與關(guān)系圖并延伸到子系統(tǒng)A/B的模塊設(shè)計細(xì)則包括功能描述、接口屬性、數(shù)據(jù)結(jié)構(gòu)與算法、補充說明等。目前已有665人學(xué)習(xí)下載能為團隊提供清晰的模塊設(shè)計指導(dǎo)也為后期維護、升級和知識傳遞打下扎實基礎(chǔ)。1. 模塊設(shè)計文檔寫不到位的“詳細(xì)”就是給項目埋雷我見過太多項目死在一種奇怪的節(jié)奏里架構(gòu)評審時大家把模塊邊界畫得漂漂亮亮等代碼跑到第三個迭代新人接手一個模塊讀代碼讀了兩天依然不知道這個模塊到底對外承諾了什么、邊界在哪里、哪些異常是“設(shè)計內(nèi)允許”的。這時候回頭找設(shè)計文檔要么只有半頁概要要么是一堆照著敲鍵盤也能想到的流水賬。這個標(biāo)題里的“很詳細(xì)的”四個字恰恰是區(qū)分文檔能不能在項目中期救命的分水嶺。詳細(xì)不是字?jǐn)?shù)多而是把接口契約、狀態(tài)流轉(zhuǎn)、異常分支、數(shù)據(jù)歸屬這些“容易在代碼里藏雷”的東西提前用白紙黑字釘死。這篇筆記面向的讀者是正在寫模塊文檔的人或者被“文檔沒什么用”拖累過的開發(fā)者我把一線實際在用的文檔骨架、寫法邊界和踩坑路徑拆開講照著搭你的模塊文檔至少能在項目中期救人一次。2. 先說清楚“詳細(xì)”到底指什么以及文檔寫給誰看2.1 詳細(xì)不等于長篇大論是把決策場景寫進文檔很多開發(fā)者一聽到“詳細(xì)文檔”第一反應(yīng)是把方法名、變量名、函數(shù)調(diào)用關(guān)系全部抄一遍。這種文檔寫到一半自己就想撕掉因為代碼一改文檔就得跟著改根本維護不動。我理解的“詳細(xì)”是圍繞“設(shè)計決策”做完整展開為什么這個模塊的緩存放在這一層為什么錯誤碼要定義成三段式為什么超時閾值默認(rèn)配 3 秒這些決策在代碼里是看不出來的或者說只能通過反編譯式的考古才能還原而一份詳細(xì)文檔要做的就是把這些決策連同背后的備選方案、權(quán)衡過程一并記錄下來。寫法上我常用“決策模板”來約束自己先寫“這里做了什么決定”再寫“當(dāng)時有哪兩個備選方案”最后補一段“為什么沒有選另一個”。不要小看這段“為什么”項目到第三輪重構(gòu)時新人問得最多的恰恰是“這里為什么要這樣”。如果文檔里只有結(jié)論沒有理由那和代碼注釋里寫“這里不要動”沒有任何區(qū)別都屬于沒法驗證的黑匣子。實際操作時我會給每個重要設(shè)計點分配一個編號比如 MDD-INT-001 表示“對外接口設(shè)計第 1 條”MDD-CACHE-002 表示“緩存策略第 2 條”。后續(xù)代碼評審、缺陷單、變更記錄里都可以引用這個編號關(guān)聯(lián)到具體設(shè)計決策。這種做法最大的好處是當(dāng)某段邏輯被改出問題時可以順編號查回設(shè)計初衷判斷到底是“實現(xiàn)偏離了設(shè)計”還是“設(shè)計本身就該被推翻”。2.2 一份模塊文檔的四個真實讀者以及他們各自要什么寫文檔之前先想清楚給誰看文檔的顆粒度和語言風(fēng)格完全不一樣。在我?guī)н^的項目里模塊設(shè)計文檔至少面對四類讀者新加入的開發(fā)者、負(fù)責(zé)聯(lián)調(diào)的上下游同事、做代碼評審的資深工程師、半年后的自己。新開發(fā)者需要文檔回答“這個模塊的入口在哪里、狀態(tài)有幾種、我改哪一塊不會炸”上下游同事需要文檔回答“接口的字段含義、超時和重試機制、錯誤碼在什么場景出現(xiàn)”評審專家需要文檔回答“這個設(shè)計有沒有遺漏的并發(fā)場景、有沒有不一致的狀態(tài)”半年后的自己需要文檔回答“當(dāng)初為什么做了這個看起來很奇怪的決定”。這四類訴求落到實際寫作上會直接推導(dǎo)出文檔的結(jié)構(gòu)比重。比如接口契約部分面向上下游的部分要寫得像一份正式的協(xié)議字段類型、取值范圍、默認(rèn)值、異常分支一個都不能少而面向新人的部分則要在模塊概述里用一段話講清“這個模塊本質(zhì)上是做什么的”這句話寫不好新人就會把讀文檔變成盲目翻代碼。我見過最失敗的模塊概述是“本模塊負(fù)責(zé)訂單模塊與支付模塊之間的交互”這句話毫無信息量等于沒說。因此我會在文檔開頭強制要求寫一段“模塊的一句話職責(zé)”并配上“本模塊不做什么”的負(fù)面清單。負(fù)面清單非常有用它劃定邊界的方式比正面描述更硬——比如“本模塊不做支付通道的業(yè)務(wù)校驗”這句話能直接攔住一堆需求外溢。四個讀者的訴求對應(yīng)到文檔里就是四個必備板塊一句話職責(zé)、接口契約表、狀態(tài)機表、決策記錄表。這四塊占滿文檔基本就到了“詳細(xì)”的水位線。3. 詳細(xì)版模塊設(shè)計文檔的骨架從概述到上線清單3.1 文檔骨架與章節(jié)驗收標(biāo)準(zhǔn)我日常會維護一份模塊設(shè)計文檔模板按固定順序排列章節(jié)每章都有明確的驗收標(biāo)準(zhǔn)。下面這張表是我在當(dāng)前團隊里推行的版本章節(jié)順序嚴(yán)格按照“先講清為什么 → 再講怎么對外 → 再講內(nèi)部怎么轉(zhuǎn) → 最后講怎么上線”的邏輯推進。章節(jié)必填內(nèi)容驗收標(biāo)準(zhǔn)模塊概述一句話職責(zé)、負(fù)面清單、模塊邊界圖文字版新人讀 5 分鐘能說出“這模塊干什么、不干什么”術(shù)語與縮寫模塊內(nèi)專用詞、業(yè)務(wù)黑話、簡稱全稱對照新人對接口描述里的縮寫不再產(chǎn)生歧義接口設(shè)計每個對外接口的入?yún)⒊鰠?、錯誤碼、超時、重試上下游同事能直接按文檔寫聯(lián)調(diào)代碼不用再來問數(shù)據(jù)設(shè)計模塊持有的數(shù)據(jù)表或緩存鍵、數(shù)據(jù)歸屬、生命周期評審時能說清“這份數(shù)據(jù)歸誰管什么時候刪”狀態(tài)機設(shè)計核心狀態(tài)列表、狀態(tài)遷移表、非法遷移說明所有狀態(tài)流轉(zhuǎn)都覆蓋無非法遷移關(guān)鍵流程時序主流程、分支流程、異常流程的時序描述流程圖缺失時靠文字也能還原完整鏈路異常與兜底系統(tǒng)級異常、依賴不可用、數(shù)據(jù)不一致的兜底策略每個異常分支都有明確響應(yīng)動作不是“報錯就行”配置項開關(guān)、閾值、超時參數(shù)及其默認(rèn)值、修改影響運維能不看代碼就知道改哪個配置有什么影響上線與回滾清單依賴發(fā)布順序、數(shù)據(jù)庫變更、回滾步驟按清單操作可獨立完成上線不需要再找研發(fā)這是整體框架實際寫的時候每一節(jié)都要落到具體對象上不要出現(xiàn)“保證系統(tǒng)穩(wěn)定”“提高用戶體驗”這種空話。比如接口設(shè)計里寫“查詢訂單詳情”要寫成“根據(jù) orderId 查詢訂單主檔返回訂單狀態(tài)、金額、支付時間orderId 為空時返回 PARAM_ERROR 錯誤碼”這才是能指導(dǎo)聯(lián)調(diào)的細(xì)節(jié)。3.2 每一個章節(jié)里的“細(xì)節(jié)密度”怎么控制框架定了關(guān)鍵是怎么填。我的經(jīng)驗是按“場景密度”來控制一個章節(jié)如果描述了超過 3 個分支場景就要考慮拆分或配表格不要連續(xù)寫大段散文。比如接口設(shè)計中入?yún)⑿r灐I(yè)務(wù)校驗、依賴超時、依賴返回異常這四個場景可以拆成一張四列的表每列寫各自的返回碼和預(yù)期行為這樣寫比純文字節(jié)省一半篇幅信息量卻多一倍。另一個密度控制原則是“能畫表就不寫句子能用句子就不堆術(shù)語”。數(shù)據(jù)設(shè)計也不例外每個緩存鍵的格式、TTL、失效策略、寫入時機都要寫全。舉一個真實發(fā)生過的案例某個模塊的緩存鍵由“userId 商品渠道”拼接而成設(shè)計文檔里只寫了“緩存商品信息”沒寫鍵的拼接規(guī)則結(jié)果另一個團隊復(fù)用這個緩存服務(wù)時按自己的鍵格式去查查不到數(shù)據(jù)最后排查了兩天才發(fā)現(xiàn)是鍵格式不一致。這個坑完全可以用文檔里加一行“緩存鍵規(guī)則order_channel:{channel}:{userId}”來避免。配置項章節(jié)往往是被低估的。詳細(xì)文檔里要給每個配置項配一張小表寫清楚配置名、默認(rèn)值、單位、取值范圍、變更后是否需要重啟、影響面。尤其要寫“這個配置對什么業(yè)務(wù)有影響”這句話在線上緊急調(diào)參時能救命。比如某個模塊的超時配置默認(rèn) 3000ms變更影響寫“影響到所有通過該模塊轉(zhuǎn)發(fā)請求的鏈路調(diào)低會加劇超時錯誤調(diào)高會拖慢上游響應(yīng)”運維一看就知道不能隨便動。最后上線與回滾清單要寫得像手術(shù)前核對單發(fā)布這個模塊前必須先發(fā)布依賴模塊、數(shù)據(jù)庫變更腳本要在什么階段執(zhí)行、失敗了第一步做什么第二步做什么。不少團隊把回滾寫成語焉不詳?shù)摹盎貪L到上一版本”這是不合格的?;貪L清單必須具體到“執(zhí)行哪個腳本、需要改哪個配置開關(guān)、依賴方是否需要同步切回”每一條都寫成可直接執(zhí)行的命令級描述。4. 把關(guān)鍵設(shè)計視圖寫厚時序、契約、狀態(tài)機三板斧4.1 時序流程用“誰在什么條件下調(diào)用了誰”替代流程圖很多模塊文檔里貼一張非常漂亮的時序圖但看懂圖的人不多。圖不是不能放而是必須搭配文字化的“調(diào)用鏈描述”。我習(xí)慣用的是“起點 → 觸發(fā)條件 → 調(diào)用方 → 被調(diào)方 → 返回路徑 → 失敗分支”這個六要素模板每個流程寫一段結(jié)構(gòu)化文字比圖更容易在評審里被挑出漏洞。舉個例子寫“退款發(fā)起流程”時完整描述應(yīng)該是起點是用戶在退款頁點擊提交觸發(fā)條件是訂單狀態(tài)為已支付且退款單未存在調(diào)用方是退款服務(wù)被調(diào)方是支付網(wǎng)關(guān)退款接口正常返回路徑是拿到退款受理號將退款單狀態(tài)置為“受理中”失敗分支是網(wǎng)關(guān)返回余額不足此時將退款單狀態(tài)置為“失敗”同時記錄失敗原因。這段文字里任何一個環(huán)節(jié)缺失都會導(dǎo)致聯(lián)調(diào)時發(fā)現(xiàn)狀態(tài)錯亂或分支遺漏。異常流程的時序描述最為重要也是詳細(xì)文檔拉開差距的地方。寫超時場景時不能只寫“超時則失敗”要寫清楚“調(diào)用方超時設(shè)置是多少超時后是立即重試還是進入重試隊列重試次數(shù)上限幾次最終失敗后是否有補償任務(wù)掃單”。這一層寫明白后續(xù)做穩(wěn)定性建設(shè)時根本不用再考古代碼查文檔就能批量梳理薄弱點。需要明確的是時序章節(jié)要為主流程和分支流程分別編號比如 FLOW-01 是主流程FLOW-01-EXC-01 是第一個異常分支評審和排障時直接引用編號溝通效率會明顯提升。這個編號習(xí)慣能倒逼寫文檔的人把流程想全因為編不下去號通常就是流程沒想清楚。4.2 接口契約表字段、錯誤碼、超時與重試一起定接口設(shè)計是模塊文檔里對外價值最高的部分一定要用表格呈現(xiàn)。我常用的接口契約表字段包括接口名稱、方向?qū)?nèi)/對外、入?yún)⒚c類型、出參名與類型、錯誤碼、超時默認(rèn)值、是否允許重試、冪等設(shè)計。下面是一張簡化示例接口入?yún)⒊鰠㈠e誤碼超時重試與冪等createTasktaskName, planIdtaskIdPARAM_ERROR / DUP_TASK / SYSTEM_ERROR2000ms不帶重試調(diào)用方需傳 requestId重復(fù)提交返回原 taskIdqueryTaskStatustaskIdstatus, percentTASK_NOT_FOUND1000ms允許重試一次查詢天然冪等cancelTasktaskId, reasoncancelIdTASK_DONE / TASK_CANCELED3000ms不重試cancelId 用于對賬這張表里最有價值的兩個字段是“重試與冪等”和“錯誤碼”。很多系統(tǒng)出故障問題不在功能邏輯而在調(diào)用方不知道“能不能重試、重試會不會重復(fù)創(chuàng)建數(shù)據(jù)”。文檔里這個字段寫清楚直接省去大量聯(lián)調(diào)摩擦。我見過某公司的內(nèi)部接口沒有冪等設(shè)計調(diào)用方按慣性重試了三次結(jié)果創(chuàng)建了三條重復(fù)訂單最后靠人工對賬才清理掉。這種問題在接口契約表里加一行“冪等要求requestId 唯一重復(fù)請求返回原結(jié)果”就能從源頭上避免。錯誤碼的定義也要細(xì)化。三段式錯誤碼是比較通用的做法第一位表示錯誤來源參數(shù)、業(yè)務(wù)、依賴、系統(tǒng)第二位表示具體模塊后兩位表示錯誤細(xì)項。比如“業(yè)務(wù)錯-訂單模塊-訂單不存在”的編碼是 20001。文檔里要列出每個錯誤碼對應(yīng)的 HTTP 狀態(tài)碼如果有、對外提示文案、日志打印級別、是否需要告警。錯誤碼表寫不全線上排障時最痛苦因為錯誤信息往往只有一個編號翻代碼才知道是什么意思。4.3 狀態(tài)機表把最容易吵架的地方提前釘死狀態(tài)機是模塊文檔里最容易寫飄的部分也是評審時最容易產(chǎn)生爭議的部分。我的做法是不畫狀態(tài)圖直接畫狀態(tài)遷移表因為表格比圖形更不容易遺漏非法遷移。狀態(tài)遷移表至少包含四列當(dāng)前狀態(tài)、觸發(fā)動作、目標(biāo)狀態(tài)、前置條件。如果動作涉及外部依賴還要再加一列“失敗時保持原狀還是進入特殊狀態(tài)”。以任務(wù)調(diào)度模塊為例核心狀態(tài)表大致長這樣當(dāng)前狀態(tài)觸發(fā)動作目標(biāo)狀態(tài)前置條件待執(zhí)行調(diào)度器掃單命中執(zhí)行中任務(wù)未過期且執(zhí)行節(jié)點空閑執(zhí)行中執(zhí)行成功回調(diào)已完成回調(diào)簽名校驗通過執(zhí)行中執(zhí)行超時待重試已超過單次執(zhí)行時長上限待重試重試次數(shù)未達(dá)上限且間隔到期執(zhí)行中重試次數(shù) 最大次數(shù)待重試重試次數(shù)已達(dá)上限失敗重試次數(shù) 最大次數(shù)這張表每個開發(fā)者的理解成本都很低評審時大家盯著表逐行過很快就能發(fā)現(xiàn)遺漏。比如“取消中的任務(wù)”這個狀態(tài)如果表里沒有就會在后端代碼里出現(xiàn)“已取消的任務(wù)被調(diào)度器再次拉起執(zhí)行”這種詭異缺陷。狀態(tài)遷移表寫完之后我還會加一個“非法遷移清單”明確列出不允許發(fā)生的遷移路徑例如“已完成 → 執(zhí)行中”“失敗 → 待執(zhí)行”這相當(dāng)于給狀態(tài)機上了約束后續(xù)代碼實現(xiàn)時可以直接對照校驗。數(shù)據(jù)設(shè)計章節(jié)放在狀態(tài)機之后也很自然。模塊持有的數(shù)據(jù)表、緩存鍵、消息隊列主題都應(yīng)該有歸屬聲明。一條數(shù)據(jù)如果多個模塊都能改必須寫明“寫權(quán)限僅歸本模塊其他模塊只能通過接口修改”這是預(yù)防數(shù)據(jù)混亂的最強聲明。5. 模塊設(shè)計文檔的四個常見坑從寫不出來到寫了沒人看5.1 坑一文檔寫完就“過期”代碼一改文檔成廢紙現(xiàn)象文檔里描述的接口和實際代碼對不上新人看文檔寫聯(lián)調(diào)代碼一調(diào)就報錯最后只能放棄文檔直接翻代碼。原因?qū)懳臋n的人把文檔當(dāng)“一次性交付物”發(fā)布后就不再維護團隊也沒有把文檔更新納入迭代完成的定義。解決把“文檔同步更新”寫進迭代完成條件只要接口參數(shù)、狀態(tài)機、超時配置變了對應(yīng)文檔必須同 PR 提交評審。沒有這個硬約束再詳細(xì)的文檔也會在三個月內(nèi)變成坑。我個人的習(xí)慣是代碼里定義了新錯誤碼或改了字段名那天的文檔修改一定和代碼改動同時提交這個習(xí)慣養(yǎng)成了文檔的保鮮期會大幅延長。5.2 坑二文檔寫成了代碼的“鏡像”翻文檔等于翻代碼現(xiàn)象模塊文檔把類名、方法名、字段名全部抄一遍代碼怎么組織文檔就怎么寫讀者看完依然不知道模塊的邊界和取舍。原因作者誤以為“詳細(xì)”就是“全面”沒有識別哪些信息是能從代碼里直接讀出來的哪些信息是代碼里讀不出來的。解決文檔只寫“代碼里讀不到的信息”——設(shè)計決策、備選方案、邊界原因、異常權(quán)衡、配置影響。類名和方法名在 IDE 里一鍵就能看到寫進文檔純屬浪費紙張。我寫文檔時會先自問一句這一段內(nèi)容新人讀代碼能不能自己發(fā)現(xiàn)如果能刪掉如果不能認(rèn)真寫。用這個標(biāo)準(zhǔn)過濾文檔篇幅會縮掉一半但信息濃度反而更高。5.3 坑三狀態(tài)機和異常分支“寫不全”評審沒人看出來現(xiàn)象文檔里的狀態(tài)機缺了一個“重試中”狀態(tài)異常分支只寫了成功路徑和參數(shù)錯誤缺了依賴超時和依賴返回異常。評審會上大家掃一眼沒人深究結(jié)果上線后狀態(tài)卡死只能臨時補代碼。原因?qū)懳臋n的人按“腦子里已有的實現(xiàn)”來寫沒按“系統(tǒng)在極端條件下所有可能路徑”來寫評審的人則默認(rèn)作者已經(jīng)想全了不做窮舉式挑戰(zhàn)。解決評審文檔時按“惡意用戶 故障注入”的心態(tài)逐行拷問某個外部接口超時了怎么辦消息消費失敗了會導(dǎo)致哪兩個狀態(tài)不一致重復(fù)推送同一事件會觸發(fā)什么遷移每問一個就往表里補一行。這個過程沒有捷徑只有用窮舉法把狀態(tài)遷移表、異常分支表寫到?jīng)]有空行。我自己的經(jīng)驗是狀態(tài)表和分支表至少各寫兩輪第一輪按正常邏輯寫第二輪按故障假設(shè)寫。5.4 坑四文檔寫成“散文”關(guān)鍵細(xì)節(jié)淹沒在大段文字里現(xiàn)象接口描述藏在第三段話的中間錯誤碼表放在附錄里新人找“超時時間是多少”找了兩分鐘沒找到最后選擇不看了。原因作者沒有按用途組織信息也沒有為高頻查詢場景設(shè)置目錄錨點和表格索引。解決把“對外接口”“配置項”“錯誤碼”三類高頻查詢信息分別用獨立表格呈現(xiàn)并在文檔開頭的目錄說明里加一句“查接口看第 3 節(jié)、查配置看第 7 節(jié)、查錯誤碼看附錄 A”。一張能 30 秒定位到答案的文檔比一篇寫滿兩萬字但找不到出口的文檔有用得多。我現(xiàn)在寫每個模塊文檔時都要求自己做到一個只帶著“線上報錯錯誤碼 20001”訴求來的值班同學(xué)能在 1 分鐘內(nèi)查到錯誤碼含義和對應(yīng)動作。這個標(biāo)準(zhǔn)不達(dá)標(biāo)就不算寫完。6. 一個能讓文檔真正值錢的技巧用“新人自測”檢驗信息完整度我堅持用了很久的文檔驗收方法是“新人自測”找一個對這個模塊完全不了解的同事只給文檔、不給代碼讓他按文檔描述實現(xiàn)一個最小調(diào)用程序或者模擬一次完整聯(lián)調(diào)流程。自測目標(biāo)不是讓他能寫出生產(chǎn)級代碼而是看他能否回答三個問題——模塊的入口在哪、錯誤碼去哪查、狀態(tài)流轉(zhuǎn)里哪一步我可能會踩坑。如果這些問題文檔回答不了說明信息密度和結(jié)構(gòu)都還有缺口需要補的不是字?jǐn)?shù)而是關(guān)鍵索引。這個技巧會反向塑造寫文檔的習(xí)慣因為知道文檔會被別人當(dāng)“唯一資料”來用寫的時候就會主動把字段表寫全、把狀態(tài)表寫到無遺漏、把異常分支補齊、把配置說明獨立成節(jié)。我過去寫模塊文檔總是先寫流程再補接口現(xiàn)在改成先列接口和狀態(tài)再穿插流程后一種順序明顯更容易暴露“邏輯斷點”——比如發(fā)現(xiàn)某個流程里調(diào)用了 “queryInfo” 接口但接口表里根本沒有這個接口那要么是流程圖錯了要么是接口表漏了當(dāng)場就能修正。另一個值得堅持的習(xí)慣是“變更留痕”。每次因為線上問題修改了某個設(shè)計決策例如重試閾值從 3 次改成 1 次我都在文檔對應(yīng)位置加一行變更記錄寫明變更時間、原因、影響范圍。這不會花費太多時間但半年后復(fù)盤穩(wěn)定性問題時這些記錄會自動變成一張“決策演變時間線”比靠人腦回憶可靠得多。我也經(jīng)歷過不看文檔直接改代碼的時期直到有一次重構(gòu)時誤刪了一個關(guān)鍵狀態(tài)分支線上數(shù)據(jù)錯亂靠著一張舊文檔才反推出被誤刪的邏輯從那以后我再也不敢把文檔當(dāng)擺設(shè)。這不只是給團隊寫更是給一個月后的自己寫。詳細(xì)文檔真正的價值是當(dāng)你已經(jīng)忘了這段代碼的時候它還能準(zhǔn)確告訴你“當(dāng)初是怎么想的、什么不能動、為什么”。希望這篇拆解能幫到你也幫到你團隊的下一任接手人。本文還有配套的精品資源點擊獲取