動詞:一套可落地的代碼質(zhì)量與交付保障體系)
兩年前接手那個中后臺項(xiàng)目的時候我第一次在代碼評審里寫下了這樣一句評語這功能可以上線但代碼本身談不上 impeccable。當(dāng)時團(tuán)隊(duì)里有人笑我吹毛求疵有人問我這個詞是不是從英文詞典里翻出來的。但后來那次大版本的線上事故——因?yàn)橐粋€邊界條件沒覆蓋、一行看起來沒啥問題的判斷邏輯寫反了——讓所有人明白了一件事如果從第一天就把 impeccable 當(dāng)成一個可以量化的交付標(biāo)準(zhǔn)而不是一種玄學(xué)式的“追求完美”很多晚上的報警電話根本不會發(fā)生。這篇內(nèi)容想聊的就是我們后來逐步建立起來的那套質(zhì)量體系。它不依賴某個特定的工具也不需要你有多高的代碼天賦更不是那種“道理我都懂”的空話。它是一套從需求定義、提交規(guī)范、評審動作、測試鏈路到復(fù)盤臺賬的完整打法適合正在帶團(tuán)隊(duì)、或者一個人扛項(xiàng)目但不想再靠“人肉兜底”的開發(fā)者。你能看到我們踩過的坑、最終留下的配置、評審時真正起作用的提問方式以及為什么我一直堅(jiān)持impeccable 是一個動詞不是一個形容詞。1. “無可挑剔”不是潔癖而是把質(zhì)量翻譯成可執(zhí)行的標(biāo)線在動手定規(guī)范之前先得回答一個問題什么才算 impeccable如果你只能說“代碼寫得好”“測試覆蓋高”“線上沒事故”那這種定義等于沒定義。因?yàn)樗豢啥攘?、不可?fù)現(xiàn)更沒法在團(tuán)隊(duì)里傳遞。我見過很多“追求質(zhì)量”的團(tuán)隊(duì)最后變成開會時喊口號一上線就靠運(yùn)氣核心原因就是把質(zhì)量標(biāo)準(zhǔn)停留在感覺層面。1.1 把質(zhì)量拆成三個可觀察的維度我們后來把“無可挑剔”拆成了三個維度每個維度都對應(yīng)明確的、可以在日常流程中被檢查的對象功能正確性用戶場景是否完整覆蓋邊界條件是否處理異常分支是否有兜底。代碼可維護(hù)性命名是否表意、模塊邊界是否清晰、改動是否是“最小必要集合”。交付穩(wěn)定性從合并到發(fā)布的過程是否可重復(fù)、可回滾、可追溯。三個維度缺一不可。功能正確性只保證當(dāng)下能用可維護(hù)性決定三個月后改得動交付穩(wěn)定性決定出了問題你能不能睡得著覺。任何一個維度崩了另外兩個做得再漂亮最終都會以你最沒想到的方式反噬。1.2 定義“完成”的最低門檻核心動作是把“我覺得寫完了”變成“滿足以下所有條件才算寫完”。我們團(tuán)隊(duì)當(dāng)時起了個名字叫Definition of Done不是掛在文檔里裝樣子而是真的在每次提測、每次提交 PR 前逐條過。這條清單不長最初只有八條后來壓縮到六條需求拆解出的每個場景都有對應(yīng)的測試不光是“最順的那條主鏈路”。所有新增或修改的公共函數(shù)、復(fù)雜分支在 PR 描述里用自然語言解釋過一遍。控制臺無新增報錯接口無未捕獲的異常。本地環(huán)境跑通了完整的核心流程而不是只測了自己改的那一屏。改動的代碼量盡量收斂遇到“順便改一下其他文件”的情況必須單獨(dú)說明理由。自己先用“如果我是用戶”的角度把頁面點(diǎn)過一遍包括空數(shù)據(jù)、弱網(wǎng)、權(quán)限不足這些別扭的場景。你可能覺得這哪算 impeccable這不就是基本要求嗎對你說到點(diǎn)子上了。真正的“無可挑剔”是先把這個“基本要求”穩(wěn)定地、無例外地執(zhí)行到位而不是每天追逐那些花哨的架構(gòu)概念。很多項(xiàng)目的崩潰恰恰是連基本要求都守不住底部的那塊地基。1.3 讓“標(biāo)線”成為所有人共用的語言定義好標(biāo)線之后最重要的一件事是讓它變成團(tuán)隊(duì)共同的詞匯。有一次評審一個人寫了個工具函數(shù)邏輯非常繞但測試數(shù)據(jù)全過了。我問他你能不能用一句話解釋這個函數(shù)的輸入和輸出他想了半天說“反正傳進(jìn)去什么就返回什么”。那一刻我們就知道這代碼不是 impeccable——因?yàn)槿绻腥藳]法用自然語言說清楚一個函數(shù)在干什么那它進(jìn)入代碼庫之后下一次維護(hù)的人一定會在某個深夜詛咒它的作者。所以我們在標(biāo)線里加了一條不成文的規(guī)則解釋不清楚的代碼一律重寫。這條規(guī)則看著主觀但執(zhí)行起來意外地有效。因?yàn)樗浦税褟?fù)雜邏輯拆小把抽象拉回具體。代碼如此需求描述、測試設(shè)計(jì)、發(fā)布計(jì)劃也一樣——凡是說不清“為什么”的東西都不配進(jìn)入“完成”的范疇。2. 從協(xié)作動作下手把“無可挑剔”變成提交規(guī)范與門禁很多人一談質(zhì)量就想到自動化測試、流水線、覆蓋率這些當(dāng)然重要但它們管不住一個更早、更隱蔽的環(huán)節(jié)提交之前的動作。如果每個人的提交信息亂七八糟、分支策略隨性而為、PR 描述空泛無物那你后面做的所有自動化檢查都是在給一個本來就歪的墻刷漆。2.1 提交信息不是給你看的是給三個月后的自己看的我們統(tǒng)一的提交信息格式是從社區(qū)常見規(guī)范里改出來的但執(zhí)行時加了自己的細(xì)節(jié)要求type(scope): subject body // 必須寫清楚“為什么”做這個改動而不是復(fù)述代碼干了什么type 范圍明確為 feat、fix、refactor、docs、test、chore 六種。scope 寫影響的模塊或頁面比如feat(checkout): 支持優(yōu)惠券疊加。subject 一句話點(diǎn)明做的事情body 里強(qiáng)迫自己回答一個問題這次改動是為了解決什么約束我第一次逼團(tuán)隊(duì)寫 body 的時候阻力很大。有人覺得“反正在代碼里都能看到”。我拿一個真實(shí)例子回他你上個月改過一個價格計(jì)算函數(shù)把原來的四舍五入改成了向下取整代碼里只寫著Math.floor。三個月后你自己回來能記得是因?yàn)槭裁礃I(yè)務(wù)需求才這么改的嗎他說我記得大概跟活動有關(guān)。我說你看連你自己都記得模模糊糊將來接手的人只會更迷糊。提交信息真正對抗的不是現(xiàn)在而是遺忘。2.2 分支策略越簡單人心越容易守得住分支策略我們最終選擇了非常收斂的方式主干開發(fā)、短生命周期分支、統(tǒng)一通過 PR 合入。沒有 develop、release、hotfix 那一大套因?yàn)槿嗽蕉嗔鞒淘椒背鲥e的概率就越大。任何功能分支不超過兩天超過兩天就說明任務(wù)拆大了必須回需求層面重新切。這個策略的實(shí)質(zhì)邏輯是把合并沖突的可能性壓到最低把每次評審的代碼量控制在人能認(rèn)真看懂的范圍內(nèi)。你想想如果一個人每次打開 PR 都要面對 2000 行改動他哪怕再有責(zé)任心也會迅速滑向“看得頭痛、草草 approve”。門禁不是靠意志力守住的是靠控制輸入大小守住的。2.3 PR 模板把評審里追問過一百遍的問題前置我們團(tuán)隊(duì)的 PR 模板第一條是“一句話說明本次改動解決什么問題”第二條是“改動涉及的關(guān)鍵文件和風(fēng)險點(diǎn)”第三條是“測試驗(yàn)證清單手動點(diǎn)過哪些頁面、覆蓋了哪些邊界”?;闹嚨氖窃趶?qiáng)制模板之前這三條內(nèi)容絕大多數(shù) PR 里都缺失評審時候全靠人肉追問。模板的意義不是增加填寫負(fù)擔(dān)而是把那些你早晚要回答的問題放到合并之前回答。你可以直接把下面這個最小可用的 PR 模板抄走能省很多扯皮的功夫## 本次改動解決什么問題 一句話說明不寫“優(yōu)化了 XX”這類模糊表述 ## 關(guān)鍵改動與風(fēng)險點(diǎn) 列出核心文件說明為什么這里容易出問題 ## 測試驗(yàn)證清單 - [ ] 主鏈路場景 - [ ] 邊界/異常場景如空數(shù)據(jù)、超時、權(quán)限 - [ ] 相關(guān)舊功能回歸點(diǎn) ## 自查確認(rèn) - [ ] 無無關(guān)文件改動 - [ ] 公共變更已同步說明配套的還有一個自動化的基礎(chǔ)門禁腳本。我們沒在 CI 里堆一堆花哨插件只加了最堵得住問題的幾道閘#!/bin/bash # 基礎(chǔ)門禁提交前強(qiáng)制檢查 echo 檢查未解決沖突標(biāo)記 if grep -rn ^ \|^ src/; then echo 發(fā)現(xiàn)沖突標(biāo)記禁止提交 exit 1 fi echo 檢查調(diào)試殘留 if grep -rn console\.log\|debugger src/ --include*.js --include*.ts; then echo 存在調(diào)試語句請清理后重試 exit 1 fi echo 檢查文件變更規(guī)模 CHANGED_FILES$(git diff --cached --name-only | wc -l) if [ $CHANGED_FILES -gt 30 ]; then echo 單次提交變更文件過多請拆分提交 exit 1 fi echo 門禁通過你可能覺得這腳本簡陋得拿不出手但它的價值恰恰在于把團(tuán)隊(duì)本來就口頭約定過的事變成了機(jī)器強(qiáng)制攔截省去了每次 code review 時說“你怎么又提交了調(diào)試代碼”的尷尬。人際摩擦帶來的規(guī)范漏洞用自動化去補(bǔ)是最劃算的。3. 代碼評審不是走過場讓每一條評論都落在要害上代碼評審是整個質(zhì)量體系里最容易形式化的環(huán)節(jié)。很多團(tuán)隊(duì)開著開著變成“你 approve 我的、我 approve 你的”然后合并、上線、出事故。我把我們評審里真正起作用的幾條原則說透它們不復(fù)雜但每一條都曾經(jīng)攔住過線上故障。3.1 評審的第一句問“為什么”而不是“怎么改”低質(zhì)量的評審意見長這樣“這里建議用常量不要魔法數(shù)字”“這個函數(shù)名字應(yīng)該改一下”。不是說這些意見不對而是它們停留在表達(dá)層沒有觸及設(shè)計(jì)的理由。真正有攔截力的評審意見會追問“為什么這里不用價格服務(wù)里已有的折扣計(jì)算我擔(dān)心兩套邏輯會漂移?!薄盀槭裁催@個邊界值要取 0.01我懷疑精度問題會在這里爆掉?!蔽覀冏隽艘淮螌?shí)驗(yàn)連續(xù)三周統(tǒng)計(jì)評審意見的類型結(jié)果是直接寫“建議怎么改”的評論占比超過七成而問“為什么這么設(shè)計(jì)”的評論不足一成。后來我定了一條規(guī)矩評審里提方案之前先問理由。理由不成立再談方案。這一條把很多人從“復(fù)制粘貼式評審”里拉了出來逼著他們真的讀代碼而不是只看 diff 表面。3.2 評審清單不查“有沒有測試”查“測試有沒有測對”經(jīng)典的評審要求是“請補(bǔ)充測試”。但這句話幾乎不產(chǎn)生價值因?yàn)樗鼪]回答“補(bǔ)什么測試、驗(yàn)證哪條行為”。我們把評審視角切到一個更刁鉆的位置測試是否測了變更真正想要改變的那個行為。舉個例子。一次改動是修一個支付回調(diào)重復(fù)通知的 bug修法是加了冪等判斷。提交者寫的測試是“調(diào)用回調(diào)接口返回成功”——這測試當(dāng)然能過但這根本沒打到點(diǎn)子上。正確的測試應(yīng)該構(gòu)造“同一條通知連續(xù)發(fā)兩次第二次應(yīng)該被判定為重復(fù)并被忽略”。這個視角上的差別就是把反饋從“哦我補(bǔ)了個測試”升級成“我知道這道測試防線真的守住了哪個需求”。3.3 評審過的代碼合入后第一個發(fā)布周期要當(dāng)場盯這是我們踩坑換來的血淚教訓(xùn)。很多問題不是評審的時候沒看出來而是合入之后跟外部系統(tǒng)一交互牽出評審時根本想象不到的狀態(tài)。所以我們增加了“發(fā)布觀察期”這個動作改動合入后的第一個完整發(fā)布周期原作者不去開新需求而是要盯自己改的那塊有沒有異常日志、有沒有用戶反饋、指標(biāo)有沒有抖動。說白了評審?fù)ㄟ^不意味著這件事結(jié)束了它只意味著責(zé)任開始轉(zhuǎn)移——從“寫代碼的人”轉(zhuǎn)移到“為線上運(yùn)行負(fù)責(zé)的人”。這個心理上的切換比任何工具都更能讓一個人對自己的代碼長記性。下面是一張我們貼在自己項(xiàng)目文檔里的評審檢查表供你直接參考檢查項(xiàng)評審時的提問方式行為變化點(diǎn)這次改動改變了哪個具體輸入到輸出的行為邊界與異常如果參數(shù)為空、超時、被外部拒收這個函數(shù)會怎樣數(shù)據(jù)一致性失敗時有沒有留下不一致的中間狀態(tài)可恢復(fù)嗎變更局部性有沒有無關(guān)文件被卷進(jìn)來權(quán)限是否收得太寬測試質(zhì)量這個測試是否覆蓋了真正的行為變化還是在走形式可回滾性如果上線后出問題這個改動能否獨(dú)立回滾4. 驗(yàn)證鏈路搭到密不透風(fēng)測試、灰度與回歸的取舍如果說提交規(guī)范和評審是防守的前兩道閘門那測試、灰度與回歸就是最后一道縱深防線。但我也要說一句反直覺的話測試覆蓋率不是越高越好盲目追覆蓋率是另一種形式的偷懶。無效測試、為了覆蓋率湊出來的測試很多項(xiàng)目里都充斥著這種“綠色但無意義”的假安全感。4.1 測試的兵種單測、集成測試、手工探路各管一段我們的測試分層遵循一個樸素原則讓最便宜的手段守住最多的普通情況讓最貴的手段只精準(zhǔn)打擊高風(fēng)險區(qū)域。單元測試覆蓋純邏輯層、工具函數(shù)、狀態(tài)機(jī)轉(zhuǎn)換。跑起來快到無感適合卡在提交前和 CI 的每個提交上。重點(diǎn)不追求 100% 覆蓋率而是優(yōu)先覆蓋所有分支判斷和邊界取值。集成測試只挑跨模塊、跨服務(wù)的關(guān)鍵鏈路比如訂單流轉(zhuǎn)、權(quán)限拼接、支付回調(diào)。這些路徑出錯代價高值得用稍慢一點(diǎn)的測試守護(hù)。手工/探索性測試保住最核心的用戶旅程每個版本發(fā)布前由非該模塊的同事按照一份“核心路徑演武表”點(diǎn)一遍。為什么不用完全自動化因?yàn)橛行w驗(yàn)層面的問題比如按鈕位置讓人誤點(diǎn)、文案誤導(dǎo)用戶自動化測試感知不到只能靠一個有正常人類直覺的人去點(diǎn)。很多團(tuán)隊(duì)的誤區(qū)是單測覆蓋率低得可憐卻一門心思搞昂貴的端到端自動化最后兩臺機(jī)器跑一個晚上還經(jīng)常不穩(wěn)定。正確的思路是單測多到能瞬間定位問題端到端少到能穩(wěn)定跑完核心路徑。4.2 灰度發(fā)布不是可選項(xiàng)是質(zhì)量體系的一個閉環(huán)我們規(guī)定所有涉及核心交易鏈路的改動必須走小比例灰度。操作上很簡單先放 5% 的流量觀察半小時關(guān)鍵指標(biāo)錯誤率、耗時、業(yè)務(wù)轉(zhuǎn)化有沒有掉再逐步放大到 20%、50%、100%。這個過程看似拉長了發(fā)布周期但它換來的確定性是值得的。我曾經(jīng)親眼見過一次“全量發(fā)布十分鐘后發(fā)現(xiàn)價格字段讀錯”的事故就是因?yàn)榘l(fā)布的人嫌灰度麻煩直接一把梭。那次事故后我們把灰度從“推薦做法”提升到了“發(fā)布的前置條件”。灰度不是為了慢慢發(fā)布而是為了讓不可見的風(fēng)險在小范圍內(nèi)先暴露一次。你要真覺得一個改動太簡單不需要灰度那就把它定位成“低風(fēng)險改動”但需要有明確的理由說清楚為什么低風(fēng)險而不是憑著“我感覺沒啥事”。4.3 回歸不是靠點(diǎn)出所有老功能而是靠引流歷史請求回歸測試我們踩過一個大坑試圖在每次發(fā)布前把“所有相關(guān)功能”手點(diǎn)一遍。這個事根本做不完人還會因?yàn)榭菰锒┑粽嬲龖?yīng)該關(guān)心的地方。后來我們換了個思路把歷史上發(fā)生過線上問題的高風(fēng)險路徑收集成一份回歸清單每次發(fā)布前只跑這份清單再配合灰度觀察線上真實(shí)流量。怎么樣收集“歷史高風(fēng)險路徑”就是從復(fù)盤臺賬里找。每個事故和線上問題我們都會記一條“當(dāng)時是哪個鏈路出的問題、根因是什么”于是這些鏈路自動成為下一輪的回歸重點(diǎn)。若你想從今天開始這么做哪怕沒有專門的平臺用表格記錄也完全可行這也是我在下一節(jié)想講的重點(diǎn)。這里給出一份回歸清單的最小模板表格我們至今還在用業(yè)務(wù)模塊高風(fēng)險場景最近一次出問題發(fā)布前重點(diǎn)檢查訂單創(chuàng)建使用優(yōu)惠券 庫存臨界值2024-06-12實(shí)際下單驗(yàn)證金額與庫存扣減支付回調(diào)重復(fù)通知與冪等處理2024-08-30模擬重復(fù)通知檢查攔截權(quán)限數(shù)據(jù)子賬號繼承角色變更2024-03-05手動切換角色驗(yàn)證訪問5. 質(zhì)量飛輪轉(zhuǎn)起來復(fù)盤臺賬與“不二過”機(jī)制只要軟件還在演進(jìn)bug 就會產(chǎn)生線上問題一定攔不住清零。所以我最后想聊的是impeccable 不是“不出錯”而是“同樣的錯不犯第二次”的體系和習(xí)慣。這才是質(zhì)量管理的終局。5.1 復(fù)盤不是追責(zé)會是給下一次埋路標(biāo)我們每次線上事故和加急修復(fù)之后都會補(bǔ)一份精簡復(fù)盤模板只有四欄現(xiàn)象、根因、為什么現(xiàn)有流程沒攔住、下一步行動項(xiàng)。強(qiáng)調(diào)一下這四欄都必須是具體的不能出現(xiàn)“提高代碼質(zhì)量”“加強(qiáng)測試”這類寫了跟沒寫一樣的廢話。我對“為什么現(xiàn)有流程沒攔住”這一欄的要求特別苛刻因?yàn)槟懿荒茉谶@里寫出真實(shí)缺口決定了下一次會不會換上另一件馬甲繼續(xù)翻車。比如一次事故的根因是配置中心的值被覆蓋這一欄寫的是“配置變更沒有通知機(jī)制代碼發(fā)布時無法感知關(guān)聯(lián)配置被改動”對應(yīng)的行動項(xiàng)就是“配置變更需要郵件通知訂閱人”。你看這個行動項(xiàng)具體到可以直接找負(fù)責(zé)的人執(zhí)行這就是復(fù)盤的意義。5.2 用臺賬把所有事故沉淀成團(tuán)隊(duì)的公共資產(chǎn)腹案式錯誤清單聽起來很專業(yè)但其實(shí)就是一張普通的團(tuán)隊(duì)共享表格。我們叫它“不二過臺賬”每一行包含時間、模塊、現(xiàn)象、根因、預(yù)防機(jī)制、當(dāng)前狀態(tài)。這個臺賬的威力在于當(dāng)新人加入團(tuán)隊(duì)不用自己交一遍所有學(xué)費(fèi)光是通讀一遍臺賬就能知道哪些地方最脆、最容易踩坑。這是從“個人經(jīng)驗(yàn)”到“組織經(jīng)驗(yàn)”的關(guān)鍵跨越。我們記錄的條目有一個很反直覺的發(fā)現(xiàn)大多數(shù)嚴(yán)重的線上問題都不是那種“高深莫測的并發(fā) bug”而是配置錯誤、緩存未清、發(fā)布順序不對、參數(shù)被覆蓋這類的低級失誤。換句話說質(zhì)量事故通常不是輸在技術(shù)難度上而是輸在流程的疏漏上。這讓我更加堅(jiān)定復(fù)盤臺賬的價值遠(yuǎn)大于引入某個花哨的新架構(gòu)。5.3 定期抽檢質(zhì)量體系本身也需要體檢最后一個動作是每兩周抽檢一次曾經(jīng)的臺賬條目。我們會隨機(jī)挑三條向團(tuán)隊(duì)提問現(xiàn)在如果再出現(xiàn)這種場景我們能第一時間感知到嗎如果能是靠什么感知的是靠監(jiān)控、測試、還是某個人的記憶力如果答案是“靠某個人記得”那這個條目就得重新設(shè)計(jì)防線因?yàn)橄乱淮文莻€人可能不在場也可能自己都忘了。這個抽檢機(jī)制的目的很簡單把質(zhì)量體系當(dāng)成一個需要持續(xù)維護(hù)的軟件而不是一份寫完之后就再沒人看的文檔。就像代碼會腐化一樣流程和文檔也會腐化。定期體檢才能讓“不二過”真正轉(zhuǎn)起來而不是最終變成一句貼在墻上的口號。最后說兩句常常有人問我你有沒有哪一刻覺得自己的項(xiàng)目終于可以稱得上 impeccable 了我的回答是永遠(yuǎn)沒有那樣一個時刻??吹竭@條線有的讀者可能失望覺得折騰一大圈連“完美”的邊都沒摸到是不是太不劃算了我的真實(shí)體會恰恰相反。正因?yàn)橛肋h(yuǎn)不可能做到“無可挑剔”所以這套標(biāo)準(zhǔn)才有意義——它給了一個明確的方向和一套可執(zhí)行的動作讓我們每天都知道當(dāng)前在哪一步、下一步要補(bǔ)什么。不可挑剔不是終點(diǎn)它是一個持續(xù)接近的過程。今天你提交的代碼里有一處隱患但你通過自查清單在合并前抓住了它那你今天做的事情就是 impeccable 的明天你發(fā)現(xiàn)復(fù)盤臺賬里有一條新教訓(xùn)并把它變成了一條新規(guī)那這個動作也是 impeccable 的。如果你想把這套思路落地到自己的項(xiàng)目里不要一次性鋪太大的攤子。先從最后那節(jié)“不二過臺賬”開始建起再把提交模板和評審清單加進(jìn)去最后才輪到灰度機(jī)制和回歸列表。節(jié)奏慢一點(diǎn)沒關(guān)系關(guān)鍵是每一步都走扎實(shí)。等哪一天團(tuán)隊(duì)里有人主動在 PR 描述里寫清楚“為什么”的時候你會突然發(fā)現(xiàn)impeccable 已經(jīng)從一個遙遠(yuǎn)的形容詞變成了你們每天真實(shí)呼吸的工作方式。