本文以虛構人物阿哲與訂單系統為例。人物、對話與事件皆為敘事設計,不是真實公司的事故紀錄。
「為什麼上個月的訂單金額,今天下載就變了?」
阿哲看著會計傳來的截圖,把報表重新匯出一次。沒有例外、沒有紅字,檔案也正常下載。只是同一個月份,總額真的和上週不同。
他打開 Git 紀錄。那段程式是自己寫的,昨天剛改過。
幾個月前,他還很喜歡這支 Controller。那是他進公司後,第一個獨立做完的功能。
第一次交付,他只想看到畫面動起來
當時需求很單純:讓營運選擇月份,下載訂單報表。
阿哲翻出現有的訂單頁面,複製查詢,改日期條件,再把資料塞進 CSV。金額怎麼算,他還沒完全搞懂,不過結帳那邊有一段,貼過來改一下就能用。
查資料、計算金額、轉日期格式、組欄位,全擠在同一個方法裡。
同事提醒他可以拆開。他點點頭,心裡想的是另一件事:先交出去吧,需求已經做完了。
測試環境裡的訂單不多,商品價格沒改過,促銷也只有一種。畫面上的金額對得上,他按下下載,看到 Excel 裡整齊的欄位,鬆了一口氣。
那時候的「會寫程式」,對他來說有一個很明確的標準:需求進來,東西做得出來。
接下來的需求,每一個都只差一點
先是取消的訂單不要算,再來是退款要另外列。業務又想看不同通路,主管希望畫面上也有一份即時摘要。
每次修改都不大。多一個條件、多傳一個參數,再複製一小段。
阿哲動作愈來愈快。Laravel 的語法熟了,查詢不用一直翻文件,也大概記得哪裡藏著哪種判斷。他甚至覺得自己已經跨過菜鳥那一關。
直到有一天,改報表欄位會影響摘要,調整摘要又讓退款金額怪怪的。
他開始在修改前搜尋所有呼叫位置,改完再逐頁點一次。以前最怕不會寫,現在最怕漏改。
交付的功能增加了,記在腦裡的例外也增加了。他開始用「這邊比較特殊」解釋自己不敢動的地方。
然後,新的會員優惠上線。那封詢問舊報表的訊息來了。
第一次受挫,是答案明明跑出來了,卻是錯的
阿哲和同事挑了一筆訂單,沿著資料往回查。
下單時的成交金額,其實一直存在訂單裡。但報表沿用了結帳的計算方式,拿現在的商品價格和優惠規則,重新算一次過去的交易。
以前結果剛好一樣,錯誤就躲在裡面。規則一改,過去也跟著改了。
這次不能靠多塞一個 if 交差。他得先說清楚:報表要呈現的是哪個時間點的事實?退款要另外揭露,還是回頭改寫原始成交額?
確認需求後,他們先修正取值,核對受影響的報表,再補上一個能重現問題的測試:訂單成立後,即使商品調價,這筆訂單的原始成交金額也不能改變。
阿哲先讓舊程式跑那個例子。測試失敗。換成修正後的版本,再跑一次,通過。
那個紅字讓他第一次覺得,測試有點實際用途。
他以前也寫過測試,只是習慣把程式算出的答案貼成預期值。現在他會先拿一筆資料,和需求方確認答案,再問程式算得對不對。
他也曾經把爛 Code,整理成比較漂亮的爛 Code
事故過後,阿哲決定好好重構。
他把 Controller 裡的程式搬到 Service,替方法重新命名,又加了一層共用工具。檔案看起來乾淨多了。
下一次需求來,問題卻還在。原本混在一起的判斷,現在散在幾個檔案裡;他得開更多分頁,才能確認一次修改會碰到什麼。
他才注意到,自己一直用「看起來很像」判斷哪些程式應該共用。
結帳是在決定此刻要收多少,歷史報表是在呈現當時收了多少。兩邊都有價格和折扣,背後要回答的問題卻不同。
這次他只先拆開這條界線:新的優惠規則留在結帳,歷史報表讀取交易成立時留下的紀錄。輸出 CSV 的格式整理,另外處理。
有些重複暫時留著。測試沒有涵蓋的地方,他也沒有一次全部翻掉。
累積下來的,是下次遇到問題時會想到什麼
接下來一段時間,阿哲照樣交功能,也照樣寫出讓未來的自己皺眉的程式。
差別是,他會留下幾句短短的紀錄:這次原本以為什麼成立?哪一筆資料推翻了它?下次能用什麼方式早一點發現?
隔一陣子回來看,他常常還是不滿意。不過這次說得出原因:這個方法同時決定金額和輸出格式;這個測試只證明檔案能下載;這個叫「共用」的函式,藏了兩套不同規則。
那些原本只能說「怪怪的」的地方,慢慢有了可檢查的理由。
他也開始在 code review 裡追問同事。為什麼這裡值得拆,那裡卻可以先放著?如果期限只剩一天,哪個風險得先處理?
有時同事的答案也不一致。他得拿下一次變更驗證:原本預期只改一處,最後為什麼動了三處?多加的那層設計,到底有沒有幫上忙?
這些比較,才讓經驗慢慢長出內容。
同一套做法一直做下去,也可能只是愈做愈熟。若沒有人指出問題,沒有測試揭露差異,交付之後也從不回看,寫得更多未必會改變判斷。
後來,他在動手之前,多停了一下
又一次,營運來問:「可以再加一種優惠嗎?應該差不多吧?」
以前的阿哲大概已經打開編輯器,找可以複製的地方。
這次他先問:適用新訂單還是既有訂單?訂單成立後改活動,金額要不要跟著動?退款時依哪個金額計算?
問題問完,他在紙上寫了幾個例子,才開始改程式。
同事未必會注意到這個轉變。沒有什麼慶祝,也沒有哪一次提交,正式宣布他不再是菜鳥。他仍然查文件,仍然漏想情況,仍然需要別人 review。
但同樣一句「再加一種」,他已經聽得到以前聽不到的問題。
這篇故事所說的量變到質變,大致就是這樣:做過、看見結果、修正理解,再把修正帶進下一次工作。累積一段時間後,變的是他理解需求、判斷風險和選擇做法的方式。
如果要替這個過程找一個起點,不妨打開最近改過、也最不想再碰的那支程式。
挑一個讓你漏改、猜錯或反覆確認的地方,留下能重現它的例子。先把這一件事弄清楚,改完再看看:下次遇到它,你能不能早一點認出來。
延伸閱讀與證據範圍
Kent Beck,〈Canon TDD〉,2023-12-11。特別提醒把實際結果直接抄成預期值,會失去交叉檢查的意義。這是原作者的實務方法說明,不代表採用 TDD 就能保證沒有錯誤。
Sandi Metz,〈The Wrong Abstraction〉,2016-01-20。提醒勉強維持錯誤的共用設計,可能比重複更昂貴。這是原作者的工程經驗觀點,不是「重複永遠比較好」的通則。
Martin Fowler,〈Refactoring〉。原作者公開說明;重構指逐步改善內部結構、維持可觀察行為。故事中的錯誤修正與後續結構整理是兩件事。
Dastyni Loksa 等人,〈Programming, Problem Solving, and Self-Awareness: Effects of Explicit Guidance〉,CHI 2016。在兩個各為期兩週的網頁開發營隊中研究 48 名高中生。加入解題教學與反思提示等介入的一組,自行發起功能的加權完成分數較高;指定作業的完成表現則未見顯著差異。不過這是短期教學研究,且非個體隨機分派;它沒有證明職場工程師會沿著同一條路成長,也無法把效果單獨歸給反思。
本文的人物成長弧線及「回看錯誤如何形成工程判斷」是敘事與綜合分析,不是以上作者共同提出、或研究已驗證的一套固定階段模型。
相關閱讀
若想進一步拆解目前價格與成交快照的資料責任,可接著讀〈訂單與庫存共用資料庫,也要各自守住寫入入口〉。
