📌 30 秒摘要(Layer 3)

從小工到專家的實務智慧集。它不談某個技術,而是一整套「務實工匠」的心態與習慣:不重複自己(DRY)、別放任破窗(小問題不修會腐蝕整個系統)、用曳光彈快速打通端到端再逐步完善、對自己的產出負責、持續學習投資自己。是把「怎麼當一個持續變強的工程師」講到位的經典。

🗺 心智圖(Canvas)

程式設計師修煉之道

程式設計師修煉之道

The Pragmatic Programmer

🧭 務實的哲學:對自己負責

提供選項,而非找藉口

持續學習,經營自己的知識資產

🧱 DRY 與正交性

🎯 什麼情境該想到我

當你發現「同一件事在多個地方重複」、改一處就得同步改很多地方(還常漏改)時。

⚙️ 怎麼用

系統中每一項知識,都應該有單一、明確、權威的表述。

  1. 辨識重複的「知識」(不只是程式碼,還有邏輯、設定、文件、schema)。
  2. 抽取到單一來源:共用函式/模組/常數/設定;讓其他地方引用它。
  3. 改動只需改一處 → 消除「漏改造成不一致」的整類 bug。
  4. 注意「假重複」:長得像但會各自獨立演變的,別強行合併(否則耦合出問題)。

★ 「重複的知識」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。這代表它原本是好幾個不同的東西,被硬湊在一起——拆回去(行內函式 之後再各自整理)。

🔗 相關工具

  • 工具-提煉函式 —— 發現重複之後最常用的消除手法,DRY 說「該做」,這張說「怎麼做」
  • 工具-程式碼壞味道 —— 反過來用:從壞味道清單(尤其 Duplicated Code)反推哪裡違反了 DRY
  • 工具-封裝變化點 —— 互補視角:DRY 消除的是「重複」,這張隔離的是「會變的部分」,兩者常一起出手

每一項知識在系統中 都應只有單一、明確、權威的表述

正交性:降低元件耦合 改一處不牽動全身

🪟 別放任破窗(軟體熵)

🎯 什麼情境該想到我

當你想「先湊合一下、之後再修」,或看到程式碼已經很亂而放任它繼續爛時。

⚙️ 怎麼用

一扇破窗(沒修的爛程式碼、爛設計、爛決策)會招來更多破窗。 一旦「反正已經很亂」的心態蔓延,系統會加速腐爛(軟體熵增)。

  1. 看到破窗就修,或至少擋起來:立刻修小問題,或加 TODO/工單追蹤,別放著不管。
  2. 維持乾淨的基準線:呼應童子軍規則(工具-註解是必要之惡 所屬的整潔紀律)——離開時比來時更乾淨。
  3. 別破第一扇窗:你自己也別為了趕而留下「反正只有一個」的爛攤子。

★ 為什麼「一扇破窗」會擴散:機制

這不是道德勸說,是團隊常規(norm)的形成機制:

  1. 工程師寫新程式碼時,會參考周圍的程式碼決定「這裡的標準是什麼」。
  2. 周圍都是乾淨的 → 「我也該寫乾淨」;周圍有 3 個 // TODO: fix later 和 1 個被註解掉的區塊 → 「這裡的標準是能跑就好」。
  3. 每個人都做出局部合理的決定,整體品質就單向往下。

所以破窗的代價不是那一扇窗本身,而是它改變了下一個人對標準的判斷。這也是為什麼修小問題的回報比看起來大。

★ 讓「不再破窗」自動化:棘輪(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 擦亮。

🔗 相關工具

放任一個小爛攤子 會招來更多——看到就修

🎯 曳光彈與原型

🎯 什麼情境該想到我

當你面對一個大又不確定的系統,不知從何下手、擔心各部分兜不起來時。

⚙️ 怎麼用

「曳光彈」是先射一發能看見軌跡的子彈來校準——在軟體上就是先打通一條最細的端到端路徑(能跑、能驗證),再逐步長肉。

  1. 做一個 walking skeleton:從 UI → 邏輯 → DB 的最小可運作骨架,貫穿所有層與整合點。
  2. 及早暴露整合風險:端到端先通,最難的「各部分兜不兜得起來」提前驗證。
  3. 在骨架上增量長出功能。

曳光彈 ≠ 原型:曳光彈是保留並持續發展的真實骨架;原型是探索用、用完即丟。

★ 為什麼「端到端先通」比「單元完整」重要

❌ 分層開發:先把 DB 層做完 → 再做邏輯層 → 再做 API → 最後接前端
   ★ 最大的風險(各層接不接得起來、需求對不對)留到最後才暴露
   ★ 而且前三個月沒有任何「能跑的東西」可以展示或驗證

✅ 曳光彈:先打通一條「最細但完整」的路徑
   前端一個按鈕 → API → 一條業務邏輯 → 一張表 → 回傳 → 畫面顯示
   ★ 第一週就知道:技術選型行不行、各層接得起來嗎、部署流程通嗎

★ 核心價值是「把最大的不確定性提前」,而不是「快速做出東西」。
它是風險管理工具,不是開發速度工具。

★ 骨架該包含什麼(判準很明確)

□ ★ 貫穿「所有層」:UI → API → 業務邏輯 → 資料存取 → 資料庫
□ ★ 貫穿「所有整合點」:認證、第三方 API、訊息佇列、快取
□ ★ 包含「部署」:能真的部署到一個環境並被存取
□ ★ 包含「測試」:至少一個端到端測試跑得起來
□ ★ 包含「可觀測性」:log、metric、trace 從第一天就接上

★ 最後兩項最常被跳過,但它們是骨架價值的一半——
能跑但無法測試、無法觀測的骨架,不是可以長肉的骨架。

★ 曳光彈 vs 原型(這個區分是實質的)

★ 曳光彈原型 Prototype
目的★ 驗證「架構可行」並成為正式基礎★ 探索「這個想法對不對」
品質★ 正式品質(只是功能少)可以很髒
之後★ 保留並持續長肉★ 丟棄
範圍很窄但很深(貫穿所有層)可能很寬但很淺
測試★ 有通常沒有

★★ 最危險的錯誤:把原型當曳光彈用。
「這個 demo 看起來能跑,就直接接著做下去吧」——
那些為了 demo 而走的捷徑(寫死的資料、沒有錯誤處理、沒有測試)會永遠留在系統裡。
決定一個東西是原型還是曳光彈,要在「開始寫之前」就講清楚。

★ 怎麼選第一條路徑

選不選
★ 技術風險最高的那條(用了沒經驗的技術、整合最複雜的)最簡單的那條
★ 能驗證核心假設的那條最有商業價值的那條(那是第二優先)
貫穿層數最多的只在單層內的

★ 例:做一個 AI 客服系統

第一條曳光彈不該是「登入功能」,而該是
「使用者問一句話 → 檢索 → LLM → 回應 → 顯示 → 記一筆 trace」——
因為那條路徑上有所有的未知數。

★ 之後怎麼長肉

骨架通了之後:
① 在骨架上「加寬」——同一層再加功能(第二個 API、第三個查詢)
② 需要時「加深」——把某一層做紮實(快取、批次、最佳化)
★ 每一次都保持「隨時可部署、隨時可 demo」

★ 這正是 保持可以發布 與持續交付的思路:
系統永遠處在「能動」的狀態,只是功能多寡不同。

★ 它解決的三個真實問題

問題怎麼解
整合地獄整合從第一天就存在,而不是最後才發生
★ 回饋太慢使用者/PM 第一週就看得到東西,需求誤解提早被發現
團隊不知道進度「骨架通了」是一個明確的里程碑

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意 / 什麼時候不適用

  • ★★ 不要讓它變成原型。骨架的每一行都是正式程式碼,捷徑會留下來。
    → 有測試、有錯誤處理、有 code review。
  • ★ 骨架必須「窄」。貫穿所有層,但每一層只做最少的事。
    骨架太寬 = 你其實是在做完整功能,失去了「快速驗證」的價值。
  • 不要跳過部署。「在我機器上能跑」的骨架沒有驗證到最大的風險之一。
  • 需求極度不明確時,原型才是對的。曳光彈假設你知道要做什麼、不確定的是「做不做得出來」;
    如果不確定的是「該做什麼」,先做丟棄式原型。
  • 既有系統不適用。曳光彈是新專案的手法;既有系統要用 工具-接縫 與絞殺者模式。
  • ★ 骨架通了不代表風險沒了。規模、效能、邊界情況的風險還在——它驗證的是「可行性」不是「可用性」。

🔗 相關工具

曳光彈:能保留的端到端骨架 (walking skeleton)

原型:用完即丟的探索

Link to original

🧰 這本書給我的工具

✨ 關鍵重點(Layer 1–2)

  • DRY:系統中每一項知識都應有單一、明確、權威的表述。
  • 破窗理論:放任一個小爛攤子,會招來更多——看到就修,別讓熵累積。
  • 曳光彈 vs 原型:曳光彈是能保留的端到端骨架(walking skeleton);原型是用完即丟的探索。
  • 正交性:降低元件間的耦合,改一處不牽動全身。
  • 對自己負責:提供選項而非藉口;持續學習、經營知識資產。

💬 金句原文(Layer 0)

  • 「別讓破窗留著不修。」
  • 「每一項知識,在系統中都應該只有單一、權威的表述。」

🔗 相關