📌 30 秒摘要(Layer 3)
從小工到專家的實務智慧集。它不談某個技術,而是一整套「務實工匠」的心態與習慣:不重複自己(DRY)、別放任破窗(小問題不修會腐蝕整個系統)、用曳光彈快速打通端到端再逐步完善、對自己的產出負責、持續學習投資自己。是把「怎麼當一個持續變強的工程師」講到位的經典。
🗺 心智圖(Canvas)
程式設計師修煉之道
Link to original 程式設計師修煉之道
The Pragmatic Programmer
🧭 務實的哲學:對自己負責
提供選項,而非找藉口
持續學習,經營自己的知識資產
🧱 DRY 與正交性
🎯 什麼情境該想到我
當你發現「同一件事在多個地方重複」、改一處就得同步改很多地方(還常漏改)時。
⚙️ 怎麼用
系統中每一項知識,都應該有單一、明確、權威的表述。
- 辨識重複的「知識」(不只是程式碼,還有邏輯、設定、文件、schema)。
- 抽取到單一來源:共用函式/模組/常數/設定;讓其他地方引用它。
- 改動只需改一處 → 消除「漏改造成不一致」的整類 bug。
- 注意「假重複」:長得像但會各自獨立演變的,別強行合併(否則耦合出問題)。
★ 「重複的知識」vs「長得像的程式碼」:一個判斷法
問:如果其中一處因為需求改變而必須改,另一處也必須跟著改嗎?
答案 意義 動作 必須,否則就是 bug 真重複:同一份知識有兩個副本 合併成單一來源 不一定,它們可能各自演變 假重複:只是現在碰巧長得像 保持分開 // 看起來重複,其實是兩份不同的知識 func validateSignup(u User) error { if len(u.Name) > 50 { … } } func validateShipping(a Addr) error { if len(a.Name) > 50 { … } }使用者名稱上限和收件人姓名上限碰巧都是 50。合併成
validateName(s)之後,哪天物流商把收件人欄位改成 35 字——你要嘛改壞註冊流程,要嘛再拆回來。錯誤的抽象比重複更貴:拆開一個被多處依賴的抽象,比合併兩份重複難得多。★ 三次法則(Rule of Three)
- 第一次:寫。
- 第二次:複製,但記下來。
- 第三次:這時你已經看到三個實例,知道哪些部分真的一樣、哪些會變——這才是提煉抽象的時機。
只看兩個例子就抽象,抽出來的參數與分支常常猜錯方向。
★ DRY 不只管程式碼:最有價值的是「跨格式」的重複
最常出事的重複,不是兩個函式長得像,而是同一份知識以不同格式存在好幾處:
重複的知識 典型的多個副本 單一來源的做法 資料結構 DB schema、Go struct、API 文件、前端 TS 型別 從一處產生其他: sqlc(SQL → Go)、OpenAPI/protobuf → Go 與 TS列舉值 Go 常數、DB CHECK 約束、前端下拉選單 常數定義一處, go generate+stringer產生字串方法設定預設值 程式碼、README、Helm chart 程式碼是權威,文件從程式碼產生或只寫「見 X」 業務規則 前端驗證、後端驗證 後端是權威;前端驗證只是 UX,不重複定義規則的細節(或共用 JSON Schema) 產生出來的程式碼要 commit 並在 CI 檢查是否最新(產生後
git diff --exit-code),不然「單一來源」只是理論。★ 什麼地方刻意不 DRY
- 測試程式碼:測試偏好 DAMP(Descriptive And Meaningful Phrases)——每個測試案例讀起來完整、自足,比共用一堆 helper 更重要。測試失敗時,讀者應該不用跳三個檔案才知道準備了什麼資料。
- 微服務之間:兩個服務各自定義同樣的 DTO,比共用一個「common」套件讓它們綁在一起好。共用程式庫會變成部署耦合(改一個欄位,所有服務要一起升級)。
- Go 慣用語:Go 社群有句話「一點複製勝過一點依賴」(a little copying is better than a little dependency)——為了一個 10 行的函式引入一個外部模組,通常不值得。
🧪 我實際套用的紀錄
- 2026-07-15:(待填)
⚠️ 注意
DRY 是消除「知識重複」,不是消除「所有相似的字元」;過度 DRY 會造成錯誤耦合。
過度 DRY 的症狀:一個共用函式有 4 個 bool 參數、裡面一堆
if caller == X。這代表它原本是好幾個不同的東西,被硬湊在一起——拆回去(行內函式 之後再各自整理)。🔗 相關工具
每一項知識在系統中 都應只有單一、明確、權威的表述
正交性:降低元件耦合 改一處不牽動全身
🪟 別放任破窗(軟體熵)
🎯 什麼情境該想到我
當你想「先湊合一下、之後再修」,或看到程式碼已經很亂而放任它繼續爛時。
⚙️ 怎麼用
一扇破窗(沒修的爛程式碼、爛設計、爛決策)會招來更多破窗。 一旦「反正已經很亂」的心態蔓延,系統會加速腐爛(軟體熵增)。
- 看到破窗就修,或至少擋起來:立刻修小問題,或加 TODO/工單追蹤,別放著不管。
- 維持乾淨的基準線:呼應童子軍規則(工具-註解是必要之惡 所屬的整潔紀律)——離開時比來時更乾淨。
- 別破第一扇窗:你自己也別為了趕而留下「反正只有一個」的爛攤子。
★ 為什麼「一扇破窗」會擴散:機制
這不是道德勸說,是團隊常規(norm)的形成機制:
- 工程師寫新程式碼時,會參考周圍的程式碼決定「這裡的標準是什麼」。
- 周圍都是乾淨的 → 「我也該寫乾淨」;周圍有 3 個
// TODO: fix later和 1 個被註解掉的區塊 → 「這裡的標準是能跑就好」。- 每個人都做出局部合理的決定,整體品質就單向往下。
所以破窗的代價不是那一扇窗本身,而是它改變了下一個人對標準的判斷。這也是為什麼修小問題的回報比看起來大。
★ 讓「不再破窗」自動化:棘輪(ratchet)
對舊專案,「一次修好所有問題」不可能;但「不准變得更糟」可以做到,而且可以用 CI 強制:
做法 怎麼實作 只檢查新程式碼 golangci-lint 的 --new-from-rev=origin/main:只回報這次變更新增的問題,舊問題先不管數字只能降不能升 把目前的警告數/覆蓋率存進 repo,CI 比對:警告數 > 基準 → 失敗;< 基準 → 更新基準 碰到哪修到哪 童子軍規則:你改的那個函式,離開時比進來時乾淨一點(改個名、刪個死程式碼) 明確標記技術債 // TODO(#482)必須帶 issue 編號;沒編號的 TODO 在 review 擋下棘輪的好處:不需要排一個「清理 sprint」,品質靠日常的每個 PR 單調往上爬。
★ 哪些「破窗」要立刻修,哪些可以登記
立刻修(修的成本 < 5 分鐘 或 會擴散) 登記成債(修的成本高、不會擴散) 編譯警告、lint 警告 模組邊界畫錯 被註解掉的程式碼 缺整合測試的舊模組 誤導的名字、過時的註解 需要資料遷移的 schema 問題 不穩定(flaky)的測試——它教會大家「紅燈可以忽略」 效能問題(有量測數據佐證的) flaky test 是最危險的破窗:一旦團隊習慣「CI 紅了就重跑」,真正的紅燈也會被重跑掉。發現就要隔離(標記跳過並開單)或修掉,不能放著。
🧪 我實際套用的紀錄
- 2026-07-15:(待填)
⚠️ 注意
緊急時可暫時妥協,但要明確記錄為技術債並排程償還,不能無聲無息地累積。
破窗理論在犯罪學上的證據有爭議(原始的都市治安政策效果至今仍有爭論);《程式設計師修煉之道》借用的是它的比喻,而軟體團隊中的常規擴散,靠 code review 和 CI 的經驗就能觀察到,不依賴那份犯罪學結論。
別把「修破窗」變成拒絕上線的藉口:目標是不讓品質單向下滑,不是每次上線前把整個 repo 擦亮。
🔗 相關工具
- 工具-程式碼壞味道 —— 破窗的具體長相,用清單認出「這裡已經破了」
- 工具-小步重構 —— 補窗的手法,一次補一塊比等大重構務實
- 工具-區分修復與掩蓋症狀 —— 品質關卡,用膠帶貼住破窗不算修,只是讓它更難看見
放任一個小爛攤子 會招來更多——看到就修
🎯 曳光彈與原型
🎯 什麼情境該想到我
當你面對一個大又不確定的系統,不知從何下手、擔心各部分兜不起來時。
⚙️ 怎麼用
「曳光彈」是先射一發能看見軌跡的子彈來校準——在軟體上就是先打通一條最細的端到端路徑(能跑、能驗證),再逐步長肉。
- 做一個 walking skeleton:從 UI → 邏輯 → DB 的最小可運作骨架,貫穿所有層與整合點。
- 及早暴露整合風險:端到端先通,最難的「各部分兜不兜得起來」提前驗證。
- 在骨架上增量長出功能。
曳光彈 ≠ 原型:曳光彈是保留並持續發展的真實骨架;原型是探索用、用完即丟。
★ 為什麼「端到端先通」比「單元完整」重要
❌ 分層開發:先把 DB 層做完 → 再做邏輯層 → 再做 API → 最後接前端 ★ 最大的風險(各層接不接得起來、需求對不對)留到最後才暴露 ★ 而且前三個月沒有任何「能跑的東西」可以展示或驗證 ✅ 曳光彈:先打通一條「最細但完整」的路徑 前端一個按鈕 → API → 一條業務邏輯 → 一張表 → 回傳 → 畫面顯示 ★ 第一週就知道:技術選型行不行、各層接得起來嗎、部署流程通嗎★ 核心價值是「把最大的不確定性提前」,而不是「快速做出東西」。
它是風險管理工具,不是開發速度工具。★ 骨架該包含什麼(判準很明確)
□ ★ 貫穿「所有層」:UI → API → 業務邏輯 → 資料存取 → 資料庫 □ ★ 貫穿「所有整合點」:認證、第三方 API、訊息佇列、快取 □ ★ 包含「部署」:能真的部署到一個環境並被存取 □ ★ 包含「測試」:至少一個端到端測試跑得起來 □ ★ 包含「可觀測性」:log、metric、trace 從第一天就接上★ 最後兩項最常被跳過,但它們是骨架價值的一半——
能跑但無法測試、無法觀測的骨架,不是可以長肉的骨架。★ 曳光彈 vs 原型(這個區分是實質的)
★ 曳光彈 原型 Prototype 目的 ★ 驗證「架構可行」並成為正式基礎 ★ 探索「這個想法對不對」 品質 ★ 正式品質(只是功能少) 可以很髒 之後 ★ 保留並持續長肉 ★ 丟棄 範圍 很窄但很深(貫穿所有層) 可能很寬但很淺 測試 ★ 有 通常沒有 ★★ 最危險的錯誤:把原型當曳光彈用。
「這個 demo 看起來能跑,就直接接著做下去吧」——
那些為了 demo 而走的捷徑(寫死的資料、沒有錯誤處理、沒有測試)會永遠留在系統裡。
決定一個東西是原型還是曳光彈,要在「開始寫之前」就講清楚。★ 怎麼選第一條路徑
選 不選 ★ 技術風險最高的那條(用了沒經驗的技術、整合最複雜的) 最簡單的那條 ★ 能驗證核心假設的那條 最有商業價值的那條(那是第二優先) 貫穿層數最多的 只在單層內的 ★ 例:做一個 AI 客服系統
第一條曳光彈不該是「登入功能」,而該是
「使用者問一句話 → 檢索 → LLM → 回應 → 顯示 → 記一筆 trace」——
因為那條路徑上有所有的未知數。★ 之後怎麼長肉
骨架通了之後: ① 在骨架上「加寬」——同一層再加功能(第二個 API、第三個查詢) ② 需要時「加深」——把某一層做紮實(快取、批次、最佳化) ★ 每一次都保持「隨時可部署、隨時可 demo」★ 這正是 保持可以發布 與持續交付的思路:
系統永遠處在「能動」的狀態,只是功能多寡不同。★ 它解決的三個真實問題
問題 怎麼解 整合地獄 整合從第一天就存在,而不是最後才發生 ★ 回饋太慢 使用者/PM 第一週就看得到東西,需求誤解提早被發現 團隊不知道進度 「骨架通了」是一個明確的里程碑 🧪 我實際套用的紀錄
- 2026-07-15:(待填)
⚠️ 注意 / 什麼時候不適用
- ★★ 不要讓它變成原型。骨架的每一行都是正式程式碼,捷徑會留下來。
→ 有測試、有錯誤處理、有 code review。- ★ 骨架必須「窄」。貫穿所有層,但每一層只做最少的事。
骨架太寬 = 你其實是在做完整功能,失去了「快速驗證」的價值。- 不要跳過部署。「在我機器上能跑」的骨架沒有驗證到最大的風險之一。
- 需求極度不明確時,原型才是對的。曳光彈假設你知道要做什麼、不確定的是「做不做得出來」;
如果不確定的是「該做什麼」,先做丟棄式原型。- 既有系統不適用。曳光彈是新專案的手法;既有系統要用 工具-接縫 與絞殺者模式。
- ★ 骨架通了不代表風險沒了。規模、效能、邊界情況的風險還在——它驗證的是「可行性」不是「可用性」。
🔗 相關工具
- 保持可以發布 —— 從第一天就維持可部署
- 增量式編程、使用短迭代增量發布 —— 骨架上長肉的節奏
- 提早集成頻繁集成 —— 整合風險提前
- 使用演示獲得頻繁反饋 —— 骨架讓 demo 從第一週就可能
- 工具-部署管線與持續交付 —— 骨架該包含部署
- 工具-接縫 —— 既有系統的對應手法
- 程式設計師修煉之道
曳光彈:能保留的端到端骨架 (walking skeleton)
原型:用完即丟的探索
🧰 這本書給我的工具
- 工具-DRY原則 — 發現重複的知識/程式碼時
- 工具-破窗理論與軟體熵 — 面對「先湊合一下」的小妥協時
- 工具-曳光彈開發 — 面對大又不確定的系統、不知怎麼開始時
✨ 關鍵重點(Layer 1–2)
- DRY:系統中每一項知識都應有單一、明確、權威的表述。
- 破窗理論:放任一個小爛攤子,會招來更多——看到就修,別讓熵累積。
- 曳光彈 vs 原型:曳光彈是能保留的端到端骨架(walking skeleton);原型是用完即丟的探索。
- 正交性:降低元件間的耦合,改一處不牽動全身。
- 對自己負責:提供選項而非藉口;持續學習、經營知識資產。
💬 金句原文(Layer 0)
- 「別讓破窗留著不修。」
- 「每一項知識,在系統中都應該只有單一、權威的表述。」
🔗 相關
- 專業態度見 程式設計師的職業素養;整潔紀律見 程式碼整潔之道