🎯 什麼情境該想到我
當你不確定程式碼、功能或進度的真實狀態,而是靠感覺或主觀回報在判斷的時候。
章導言引 Bill Nye:「一步行动,胜过千万专家的意见。」這一章的目標是「确保你明确知道项目的正确状态,而不是主观臆测」。
| 習慣 | 回饋的來源 | 良策 |
|---|---|---|
| 守護天使 | 程式碼 | 使用自動化的單元測試 |
| 先用它再實現它 | 呼叫端 | 把 TDD 當成設計工具 |
| 不同環境就有不同問題 | 不同平台 | 在每個支援的平台上跑測試 |
| 自動驗收測試 | 客戶的業務規則 | 為核心業務邏輯建立客戶可驗證的測試 |
| 度量真實的進度 | 剩餘工作量 | 度量剩下的工作量 |
| 傾聽用戶的聲音 | 使用者 | 每個抱怨背後都藏著一個事實 |
出處:Subramaniam & Hunt《高效程式設計師的 45 個習慣》第 5 章。引文保留原書簡體字。
⚙️ 怎麼用(逐項)
守護天使
Guard with Tests 什麼時候會冒出來:改一個地方就怕弄壞別的地方/不敢重構因為沒有安全網/想改完馬上知道有沒有破壞既有行為/每次改完都提心吊膽等別人回報
魔鬼的誘惑:「单元测试只会浪费时间,我们现在正处于关键时刻。」
良策:「使用自动化的单元测试。好的单元测试能够为你的代码问题提供及时的警报。如果没有到位的单元测试,不要进行任何设计和代码修改。」
書中重點:
- 「忘掉“测试”这个词。就把它看作是一个极好、编写能产生反馈的代码的技术。」過去開發者寫臨時的輸出語句或測試樁,檢查完就丟掉;單元測試是把它們保留下來、自動化、持續執行。
- 側欄〈清楚自己要測試的內容〉——一個真實的 bug:讀者 David Bock 深夜遷移建置工具時,單元測試突然失敗,退回原本的版本仍然失敗。原因是一個測試記錄了執行時的時間,而被測的方法有 off-by-one 錯誤:在晚上 11 點到 12 點之間呼叫,會回傳當天中午而不是隔天。教訓:
- 測試要可重複:不要依賴目前時間、本機 IP。
- 測試邊界條件(11:59:59 和 0:00:00)。
- 不要放過任何失敗的測試:每天有幾十個失敗的測試,偶發的失敗就不會有人注意。
- 每次建置都自動跑測試:「如果测试没有通过(或者没有测试),那就像编译没有通过一样糟糕。」再加上背景的建置機器持續跑——這就是守護天使。
- 書中列出單元測試的其他價值:讓程式碼更健壯、是好的設計工具、讓你有信心重構、是除錯時的探針、是可信的文件、是學習新 API 的工具(為新 API 寫「學習用測試」,日後 API 有不相容的變化時也能發現)。
切身感受:「如果代码没有测试,你会觉得很不舒服,就像是在高空作业没有系安全带一样。」
平衡的藝術:
- 單元測試是值得投資的優質股,但簡單的屬性存取方法不值得測。
- 不寫單元測試的藉口,很多源自程式碼設計的缺陷;抗議越強烈,設計越糟(見 工具-測試先行的設計)。
- 測試要達到一定的覆蓋率才真正有用。
- 不是測試越多品質越高——測不出任何問題的測試,也許是測錯了地方。
在今天的樣子:David Bock 的時間 bug 正是 Go 1.25 的 testing/synctest 要解決的問題——在測試中使用假時鐘(見 工具-測試先行的設計)。
先用它再實現它
Use It Before You Build It 什麼時候會冒出來:設計出來的介面不確定好不好用/想先從呼叫端的角度看介面長怎樣/打算先寫測試再動手實作/函式簽章一直改來改去很難用
魔鬼的誘惑:「前进,先完成所有的库代码。后面会有大量时间看用户是如何思考的。」
良策:「先用它再实现它。将TDD作为设计工具,它会为你带来更简单更有实效的设计。」(旁註:好的設計並不代表需要更多的類別)
書中的例子——井字棋:
- 一開始想到的類別有:棋盤、格子、列、欄、玩家、使用者、棋子、分數、規則……
- 第一個測試:建立棋盤,斷言它不是 null、遊戲還沒結束。先確認測試會失敗(類別不存在、編譯錯誤),再用最少的程式碼讓它通過——
GameOver直接回傳 false,等於「欺騙測試」。 - 第二個測試:設定先手玩家。這迫使你決定怎麼表示玩家:
board.SetFirstPlayer(new Player("Mark"), "X")→- 依 YAGNI(You Aren’t Gonna Need It)不需要 Player 類別 →
board.SetFirstPlayer("X")→ - 仍然有傳入任意字母、需要驗證的風險 → 最後簡化成一個布林屬性:
board.FirstPlayerPegIsX = true。
- 「我们是以Player类开始,最后却只使用了简单的布尔类型属性。」
書中強調,重點不是用一堆布林值表示一切,而是找出「成功實作特定功能的最低成本」。還沒寫的類別很容易刪掉;寫了之後,就傾向保留它。
切身感受:「只在有具体理由的时候才开始编码。你可以专注于设计接口,而不会被很多实现的细节干扰。」
平衡的藝術:
- 不要把「測試先行」和「提交前測試」混為一談:前者幫助改進設計,後者仍然必須做。
- 驗證想法或做原型時,單元測試也許不適合;但原型倉促變成正式系統時,就必須補上測試(最好重新設計)。
- 單元測試無法保證好設計,但會有幫助。
在今天的樣子:完整的節奏見 工具-紅綠重構循環;為什麼先寫測試會改善設計,見 工具-測試先行的設計。
不同環境就有不同問題
Different Makes a Difference 什麼時候會冒出來:在我電腦上跑得好好的別人卻壞掉/只在自己環境測過就宣稱沒問題/換一台機器或不同作業系統就出錯/使用者回報的問題我重現不出來
魔鬼的誘惑:「只要代码能在你的机器上运行就可以了,谁会去关心它是否可以在其他平台上工作。」
良策:「不同环境,就有不同问题。使用持续集成工具,在每一种支持的平台和环境中运行单元测试。要积极地寻找问题,而不是等问题来找你。」
書中重點:
- 有人說「那不會有什麼不同」——你可以打賭他錯了。Venkat 的測試在他的機器上通過、在同事的機器上失敗,原因是一個 .NET API 在 Windows XP 和 Windows 2003 上的行為不同。
- 側欄〈但是它在我的機器上可以工作〉:一位開發者為了提升效能,用組合語言重寫了作業系統的排程程式,結果只能在他自己的機器上執行——連買同一廠牌的硬體、裝相同的軟體都無效,最後只能放棄。
- 解法:為每個平台設定持續整合,提交後幾分鐘內就知道各平台是否正常。「构建机器的硬件成本相当于开发人员的几个小时而已。」可以用虛擬機在一台機器上跑多個作業系統版本。
切身感受:「感觉就像是在做单元测试,非但如此,而且还是跨越不同的世界的单元测试。」
平衡的藝術:
- 硬體比開發人員的時間便宜;但平台組合很多時,要選擇哪些平台需要內部測試。
- 不同的位元組順序、大小寫敏感度就能暴露很多平台 bug。
- 避免同一個錯誤收到 5 次通知的轟炸:設一個主要平台,其他的降低頻率,或統一彙整錯誤報告。
在今天的樣子:
- 容器讓「環境」變成可版本化的東西,大幅減少了這類問題;但正式環境的設定、資料量、網路條件仍然和開發環境不同。
- CI 的矩陣建置(多個 Go 版本、多個作業系統)就是這個習慣的直接實作。Go 的交叉編譯讓多平台建置很便宜,但測試仍然要在目標平台上跑——特別是牽涉檔案系統、訊號、時區的程式碼。
自動驗收測試
Automate Acceptance Testing 什麼時候會冒出來:每次改動都要人工回歸驗收/關鍵業務規則沒有人在把關/想把客戶的期望寫成可執行的驗證/上線前總要花好幾天人工點測
魔鬼的誘惑:「很好,你现在用单元测试来验证代码是否完成了你期望的行为。发给客户吧。」
良策:「为核心的业务逻辑创建测试。让你的客户单独验证这些测试,要让它们像一般的测试一样可以自动运行。」
書中重點:
- Andy 的一個專案:那個產業的標準規定,凌晨 12:00 是一天的最後一分鐘、12:01 才是第一分鐘(一般系統認為 11:59 是最後、12:00 是第一)。這個差異在驗收測試時才被發現,導致合計錯誤。
- 驗收測試和單元測試不同:要讓使用者不必學寫程式就能新增、修改測試資料。Andy 把測試資料放在純文字檔讓使用者直接改,Venkat 用過 Excel;也可以用現成的工具如 FIT(用 HTML 表格定義測試案例)。
- 側欄〈取得驗收資料〉:客戶原本有一個 Excel 的定價模型。Venkat 寫測試比較應用程式的價格輸出是否與 Excel 一致,並據此修正邏輯。
切身感受:「它像是协作完成的单元测试:你仍然是在编写测试,但从其他人那里获得答案。」
平衡的藝術:
- 不是所有客戶都能提供正確的資料——如果他們已經有正確的資料,就不需要新系統了。
- 可能會在舊系統(電腦或人工)中發現以前不知道的 bug。
- 使用客戶的業務邏輯,但不要陷入無止境的文件撰寫。
在今天的樣子:
- Go 的表格測試天然適合這個做法:把業務規則的案例放在
testdata/的 CSV 或 JSON 檔裡,由領域專家維護,測試程式讀檔逐一比對(見 工具-Go表格測試與子測試)。 - 「凌晨 12:00」的例子也說明了 通用語言 的重要:同一個詞在業務與程式裡的意義不同。
度量真實的進度
Measure Real Progress 什麼時候會冒出來:報進度老是說完成百分之九十/用投入工時來衡量做了多少/進度看起來很好卻交不出東西/想改用可運作的功能來算進度
魔鬼的誘惑:「用自己的时间表报告工作进度……不用管那些实际的工作时间,每周填满40小时就可以了。」
良策:「度量剩下的工作量。不要用不恰当的度量来欺骗自己或者团队。要评估那些需要完成的待办事项。」
書中重點:
- 公司的工時表多半是為了薪資會計,不能用來做計畫、估算或績效評估。
- 「完成了 80%」的陷阱:開發者常常回報完成了 80%,然後一週又一週停在 80%。不要算完成的百分比,要算剩下多少工作:估計 40 小時的任務,做了 35 小時後認為還需要 30 小時——這個「還需要 30 小時」才是重要的數字(誠實很重要)。
- 校正係數:完成後記錄實際花費,調整下一次的估算。估 2 天實際花了 6 天,係數是 3;除非有特殊狀況,下次的估算乘以 3。
- 側欄〈登記時間〉:Andy 的小姨子在一家大型顧問公司,每 6 分鐘登記一次時間,連「登記時間」本身都有專屬的代碼。
- 用待辦清單讓下一步可見:完成的移除,新增的先排優先順序。書中也介紹了 Scrum 的做法:每個工作日每人重新估計每個任務還需要多少小時,總和超過剩餘時間就移到下個迭代。
切身感受:「你很清楚哪些任务已经完成,哪些是没有完成的,以及它们的优先级。」
平衡的藝術:
- 6 分鐘的粒度太細,一週或一個月的粒度太粗——都不敏捷。
- 關注功能,而不是日程表。
- 一週 40 小時不等於 40 小時寫程式的時間,要扣掉會議、電話、Email。
在今天的樣子:「剩餘工作量」與校正係數的做法,在 工具-估算的藝術 中有更完整的數學(用歷史的「實際 ÷ 估算」比例校準)。停在 80% 的專案,也是蒙格 過度樂觀傾向 的典型例子。
傾聽用戶的聲音
Listen to Users 什麼時候會冒出來:覺得使用者的抱怨很沒道理/想直接解釋為什麼是他用錯/同一種客訴一直重複出現/想聽懂抱怨背後真正的問題
魔鬼的誘惑:「用户就是会抱怨……它不是一个bug,只是用户不明白如何使用而已。」
良策:「每一个抱怨的背后都隐藏了一个事实。找出真相,修复真正的问题。」(旁註:這是一個 bug)
書中的案例:
- Andy 經過技術支援部門,聽到工程師對著電話大笑:「哦,这不是bug,你只是犯了一个每个人都会犯的错误。」整個部門都在嘲笑客戶。原來客戶必須在一個晦澀的系統檔案裡設定魔術數字,否則系統不會運作——沒有錯誤訊息、不會當機,只有一個大黑畫面和一個大大的「退出」按鈕。安裝說明書裡有一行提到這件事,但 80% 的使用者都沒注意到。
- 「不管它是否是产品的bug,还是文档的bug,或者是对用户社区理解的bug,它都是团队的问题,而不是用户的问题。」
- 另一個案例:一套昂貴的工廠控制系統沒有人使用。因為第一步要輸入帳號密碼登入,而大部分工人不識字——從來沒有人問過他們。最後花了大筆錢重做成以圖片為主的介面。
- 要和真正的使用者(而不是經理或業務分析師這些代理人)交談並耐心傾聽,「即使他们说的内容很傻!」
切身感受:「对客户的那些愚蠢抱怨,你既不会生气,也不会轻视。你会查看一下,找出背后真正的问题。」
平衡的藝術:
- 沒有愚蠢的使用者。
- 只有愚蠢、自大的開發人員。
- 「它就是這樣的。」不是好答案。
- 程式碼解決不了的問題,也許可以改文件或透過培訓彌補。
在今天的樣子:
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- 回饋要快才有用:這一章的每個習慣都在縮短「犯錯」到「知道犯錯」之間的時間。測試慢到大家不想跑、驗收測試只在發布前跑一次,就失去了意義。
- 不要把指標當目標:「度量真實的進度」如果變成考核工具,就會出現填 40 小時的工時表那樣的扭曲(見 誘因扭曲)。
🔗 相關工具
- 工具-紅綠重構循環、工具-測試先行的設計 —— 本頁前兩個習慣的完整展開
- 工具-遙測與監控 —— 上線之後的回饋來源
- 高效程式設計師的45個習慣 —— 書卡