同一份完成條件,存在大神腦裡叫通靈,寫在牆上叫 AC;內容可能一模一樣,讀取權限差了一整個團隊。
昨天那場對話散會前,白板上留著一排問出來的答案:金流是非同步的、七天內未出貨才能直接退、財務的痛在對帳。最後一句結論是:把它們寫下來——不要靠通靈。
今天就是來寫的。
PM 把昨天的筆記整理了一下,五分鐘後念出第一版:
「退款功能正常運作,操作流暢,狀態顯示正確。」
平心而論,這不是偷懶。這就是大家習慣的寫法,每個人聽了都點頭——危險的地方正在這裡:每個人點頭點的,是自己腦中那個版本的「正常」。
年輕的後端工程師舉手:「驗收那天,這句話要怎麼檢查?」
「什麼意思?」
「誰、做什麼動作、看到什麼,算通過?看到什麼,算不通過?這句我讀了三遍,還是不知道程式要寫成什麼樣子才叫『正確』。」
會議室安靜了兩秒。跟 Day 18 那場安靜不一樣——那次是發現三週的工蓋在錯的假設上;這次是發現,再寫十句這種話,三週後就會回到同一個房間。
於是整個下午只做一件事:把白板上每一個「怎樣算成功」,改寫成一句可以檢查的話。
改寫的過程比想像中吵,因為每改一句,就逼出一個還沒回答的問題。
第一句。「按下退款後顯示結果」——顯示什麼結果?金流是非同步的,按下去那一秒,全世界還沒有人知道結果。所以第一句只能誠實地寫:按下退款後,這筆訂單顯示「處理中」;金流確認之前,畫面不出現「成功」兩個字。第三部最早那顆按鈕,按下去只有一種結局,這句就是衝著它來的。
第二句。那「處理中」亮到什麼時候?等金流商的確認通知。確認之後誰要知道?申請退款的客服。所以:收到確認成功的通知後,狀態變「成功」,客服收到通知。
第三句是失敗。第三部的畫面拖到聯調後期才長出「失敗」這一格,這次它在動工前就有位置:金流確認失敗後,狀態變「失敗」,客服看得到、而且可以重試。
第四句是金流串接工程師開的口:「webhook 會重送,同一筆通知可能來兩次。」這句話他在 Day 18 的聯調會議上說過一次,沒有人接話,會議記錄上也沒有這一行。上線後一筆訂單被標記兩次退款,就是那一行沉默的利息。這次,這句話當場變成一條 AC:同一筆退款通知收到兩次,錢只退一次、狀態只變一次。
第五句是替財務寫的。金流商日結、對帳檔隔天才有,所以這句話的時態是隔天:財務拿對帳檔核對,這筆退款在我們系統與金流商的紀錄一致。第三部月底那場對不上的帳,追的就是這一句的缺席。
散會時有人自嘲:「我們一個下午的產出,是五句中文。」
另一個人接:「第三部我們三週產出幾千行程式,沒有一行知道自己怎樣算完成。」
拿 Day 23 的目標句來對,會發現這五句不是新需求:「操作後能明確知道退款成功或失敗」攤開來就是前四句,「財務能對得上帳」就是第五句。AC 沒有增加任何東西,它只是把目標句磨到可以拿來驗收的細度。
一句話是不是 AC,判準只有一個:
拿著這句話站在系統前面,做完動作,答案是「過」或「不過」——而且換任何一個人來做,答案相同。
「正常」「流暢」「正確」過不了這一關。形容詞是通靈的溫床:每個人腦中都有自己的版本,而且每個版本單獨看都有道理。Day 10 說過,沒有 AC 的時候「完成」不會消失,它會分裂——工程師一版、PM 一版、客戶一版。第二部靠大神在交付前一週,一個人把所有版本悄悄對齊;第三部沒有大神,版本在上線之後、當著客戶的面對齊,用最貴的方式。
把「怎樣算好」寫下來這件事,瀑布與敏捷都有位置:瀑布把驗收依據綁在 Baseline 上,敏捷把它貼在每一條 Story 上。形式的差別 Day 10 講過,不重講。今天真正要補的是另一件事:
AC 是把每一層「成功」變成可檢查的句子;寫的過程本身,就在逼問完成語意。
Day 18 那天,團隊用一場鴉雀無聲的聯調,學到那條不等式:
HTTP 200
≠ refund accepted(金流商受理這筆退款申請)
≠ payment reversed(錢真的被退回)
≠ state confirmed(我們的系統確認並更新狀態)
≠ user goal achieved(客戶拿回錢,而且知道拿回了)
當時它是一張驗屍報告:五層「成功」,動工前回答了零層。今天,同一條不等式變成施工圖,每一層都有了可以檢查的句子:
HTTP 200
→ 按下退款後顯示「處理中」,不冒充「成功」
refund accepted
→ 「處理中」持續亮著,直到金流給出結果
payment reversed
→ 收到確認通知,狀態才變「成功」;失敗就變「失敗」、可重試
state confirmed
→ 同一筆通知來兩次,只退一次;隔天對帳檔核對一致
user goal achieved
→ 操作的客服收到通知、看得到最終結果
(這一輪的「使用者」照 Day 23 的目標句,是替客戶發動退款的客服)
Day 18 的完成語意定義表問的是「每一層由誰、用什麼機制確認」;AC 做的事,是把每一個答案釘成驗收當天可以執行的句子。寫不出句子的那一層,就是還沒想清楚的那一層。第三部的 webhook 就是這樣漏掉的——不是沒人知道,是流程裡沒有任何一個位置,逼大家把它寫成一句話。
Day 10 的場景值得倒回去看一眼:交付前一週,資深工程師一個人「再修一輪」——空值、位數、檔名、loading。那是一份又準又完整的 AC,準到連 Non-goal 都有。只是儲存位置是他的腦,讀取權限只有他自己,執行時間是驗收前一週。
今天牆上這五句話,做的是同一件事,差別只有三個欄位:儲存位置,從人腦變成牆上;讀取權限,從一個人變成所有人;執行時間,從驗收前一週,提前到動工之前。
Day 18 還說過,大神真正的功能不只是知道答案,而是會在對的時間把對的問題問出來。今天問出「這句話要怎麼檢查」的,是那位年輕的後端工程師——他沒有變成大神,他只是站上了流程裡本來就該存在的一個位置。寫 AC 這個動作,就是把「對的時間」釘死在動工之前:一句寫不成可檢查的話,當場現形,不必等三週後的聯調,也不必等某個人剛好路過。
順帶一提,資深工程師仍然在另一個專案救火。今天一整個下午,沒有人提起他。
老規矩。這個下午沒有寫任何程式,看板上沒有一張 Ticket 移動——用第三部的標準看,這是「零進度」的一天。帳怎麼記?
Scope □ 邊界照 Day 24 定案,一項沒動
Time ■ 一個下午換五句話 ← 有意識地付,買的是驗收當天沒有驚喜
Cost □
Quality □ 失敗路徑與重送防護,動工前就有了判準
Risk □ 第三部炸過的兩顆雷,這次動工前就寫進了條件
人 □ ← 這次沒有人需要通靈
對照 Day 10:同樣一份「怎樣算好」,第二部的記帳方式是人 ■——大神一個人的驗收預演,帳面乾淨得看不出任何代價。今天的帳難看多了:整個團隊、一整個下午、產出五句中文。但這五句誰來讀答案都一樣,下週任何人請假,它們都還在牆上。
通靈很便宜,直到通靈的人不在場;寫下來很貴,但貴得清清楚楚,而且一次付清。
把今天的五句話原樣留下。它是這個案子的實例;把「退款」換成你手上任何一個非同步動作,文法照用:
□ 受理|客服按下「退款」後,訂單顯示「處理中」——
金流確認之前,畫面不出現「成功」
□ 成功|收到金流確認成功的通知後,狀態變「成功」,
申請的客服收到通知
□ 失敗|金流確認失敗後,狀態變「失敗」,
客服看得到、並且可以重試
□ 重複|同一筆退款通知收到兩次,
錢只退一次、狀態只變一次
□ 對帳|退款隔天,財務拿金流商對帳檔核對,
兩邊紀錄一致
五句是同一種文法:誰、在什麼時點、看到什麼。寫不出這種句子的功能,不是太難寫,是還沒想清楚怎樣算完成——Day 10 結尾那個問題,今天原封不動地適用:那麼現在正在動工的,到底是什麼?
AC 不是驗收那天的考卷,是動工之前的照妖鏡;一句寫不成可檢查的話的需求,就是一句還沒問完的需求。
五句話貼上牆,總算可以動工了。有人開始拆 Ticket,手一順就拆成三張:前端、後端、金流——跟第三部一模一樣的拆法。
這次被攔下來了。完成條件都寫成一條線了,施工卻要切成三段各等各的?明天來講另一種切法:不要再 Backend 做完等 Frontend——先切一條細的。