程式碼是負債,不是資產#

系統的價值來自功能,而功能需要程式碼來實現——因此我們容易誤以為程式碼本身就有價值。事實上,程式碼是一種負債,是我們為了獲得功能而必須承擔的必要之惡。

把「我花了很多時間寫的程式碼」等同於「有價值的程式碼」,是典型的沉沒成本謬誤(sunk-cost fallacy)。價值不來自投入本身,而來自投入的成果。更關鍵的是,無論程式碼是否有價值,我們都必須持續投入心力維護它。

Christopher Hsee 在 1998 年的研究「Less Is Better」中發現:在一套 24 件餐具組中加入幾件破損的餐具,反而會讓整套的感知價值下降。程式碼庫也是如此——少即是多(less is better)

附帶複雜度的四種來源#

系統會隨著時間自然增長。複雜度分為兩種:**領域複雜度(domain complexity)**是問題本身的固有難度,**附帶複雜度(incidental complexity)**則是額外加進來的。

附帶複雜度有四種不同的起源,各有不同的解法:

Technical Ignorance(技術無知)#

源自經驗不足,不知不覺做了糟糕的架構決策。唯一持久的解法是持續精進技術能力——讀書、看研討會、知識分享、刻意練習。

Communal programming(社群式程式設計)是提升認知能力的有效方式。核心原則是:任何想法在進入程式碼前,必須先經過另一個人的大腦。形式包括 pair programming(兩人)和 ensemble programming(多人)。

Technical Waste(技術浪費)#

源自時間壓力下的刻意妥協——跳過測試、省略重構、繞過流程。這種妥協不帶來任何好處,本質上是破壞行為(sabotage)

解法是讓所有人理解:沒有任何場合可以跳過最佳實踐。就像不會因為想早三週交車就不測試煞車一樣。

Technical Debt(技術債)#

暫時性地選擇次優方案以換取某種收益——例如緊急修復先推上線,之後再做正式修復。這是策略性決策,本身沒有錯,但關鍵是必須有到期日。如果不是暫時的,那就不是債,而是浪費。

Technical Drag(技術拖累)#

一切讓開發變慢的東西,包括前三類,也包括文件、測試、甚至程式碼本身。自動化測試讓修改變困難(在關鍵系統中這是好事),文件需要隨變更更新,程式碼需要考慮對其他部分的影響。

「放著又不會怎樣」這句話是錯的。每一行不再為自己付費的程式碼,無論是未使用的功能、過時的文件、廢棄的分支,都應該被刪除。用不到就丟掉(Use it or lose it)。

mindmap
  root((附帶複雜度))
    Technical Ignorance
      成因:經驗不足
      解法:持續精進技術
    Technical Waste
      成因:時間壓力妥協
      解法:堅守最佳實踐
    Technical Debt
      成因:策略性決策
      解法:設定到期日
    Technical Drag
      成因:一切拖慢開發的東西
      解法:刪除不再付費的程式碼

按親密度分類程式碼#

Dan North 在 GOTO 2016 提出的分類:

分類說明
親密(intimate)剛開發的程式碼,完全了解
熟悉(familiar)常用的函式庫和工具
未知(unknown)介於中間的一切,維護成本高因為需要重新學習

大約六週後,對新程式碼的親密度就會開始衰退,最終落入「未知」類別。這個時間尺度在後續討論中會反覆出現。

處理 Legacy Code#

Legacy code 的常見定義是「我們害怕修改的程式碼」。這通常是因為**馬戲團因子(circus factor)**太低——太少人了解這段程式碼,一旦他們離開,知識就消失了。

Strangler Fig Pattern#

絞殺榕為比喻,這個模式的步驟是:

  1. 隔離:將 legacy 程式碼封裝在新的 namespace 中,建立一個 Gate 類別作為所有外部存取的唯一入口
  2. 監控:在 Gate 中加入日誌,記錄每次呼叫及其成敗
  3. 等待:部署到正式環境,觀察各功能的使用頻率
  4. 決策
    • 使用頻率最高的部分 → 重構或重建
    • 使用頻率最低或總是失敗的部分 → 評估後刪除
    • 從兩端向中間處理

如果某段 legacy 程式碼具有關鍵性或策略性,先確保使用量反映其重要性(透過改善 UI、培訓或行銷),然後重構或重建。如果不關鍵也不具策略性,就從 Gate 中刪除對應方法,並用 Try Delete Then Compile 找出因此變得無用的程式碼。

Figure 9.1: How to deal with legacy code

處理 Frozen Projects#

有時一個大功能開發完成,卻因為外部障礙(使用者培訓未完成、權限尚未取得等)而被凍結。這些凍結的專案會成為看不見的技術拖累

讓「刪除」成為預設結果#

情境處理方式
僅涉及程式碼從 main 分支還原,放到獨立分支,設定六週後刪除的標記
涉及外部資源(資料庫、服務等)在工單系統建立一張排定六週後移除所有元件的票

關鍵原則:除非有人刻意採取行動保留,否則程式碼會自動消失。這樣就不可能「不小心」增加技術拖累。

Spike and Stabilize#

Dan North 提出的模式:

  1. 將專案當作 spike 實作——盡量獨立於主應用,不做測試、不做重構,但加入監控
  2. 六週後檢查使用量
  3. 有人在用 → 重新正式實作
  4. 沒人在用 → 直接刪除(因為與主系統耦合度低,刪除很容易)

這樣既省下了可能白費的重構時間,也省下了事後拆除的心力。

Figure 9.2: How to deal with frozen projects

控制版本控制中的分支#

Git 分支幾乎不佔空間,所以人們懶得刪除——結果就是分支不斷堆積。

分支的心智負擔遠大於技術成本。理想情況下,只應該有 main 分支和可能的 release 分支,其他分支的壽命應以為單位。長壽命分支帶來的是昂貴、消耗心力、容易出錯的合併衝突。

WIP Limit(在製品限制)#

借鑑 Kanban 的概念,對分支數量設定硬性上限

  • 下限 = 工作站數量(每個獨立工作單位一條分支)
  • 上限不宜太高,以減少系統延遲
  • 一旦設定,除了團隊人數變動外,絕不破例

當分支數到達上限,上游的人無法開新分支,就會被迫去調查並解決瓶頸——這促進了團隊合作與流程改善。

Figure 9.3: How to deal with branches

處理程式碼文件#

文件(wiki、Javadoc、設計文件、教學等)要有價值,必須同時滿足三個條件:

條件定義
相關(Relevant)回答對的問題
準確(Accurate)答案是正確的
可發現(Discoverable)找得到答案

三者缺一,文件的價值就大幅下降。其中不準確的危害最大(可能導致錯誤),不相關次之(浪費瀏覽時間),不可發現最輕(僅浪費撰寫時間)。

知識編碼化的決策流程#

  1. 如果主題經常變動 → 不值得寫文件
  2. 否則,如果很少使用 → 寫文件
  3. 否則,如果可以自動化 → 自動化
  4. 否則 → 背下來

讓新進成員閱讀文件並修正不準確之處,是讓文件保持最新的有效方法。另一種始終準確的文件形式是自動化測試案例

Figure 9.4: How to deal with documentation

處理自動化測試#

刪除 Optimistic Tests(樂觀測試)#

一個永遠不會失敗的測試毫無價值。它不會增加信心,只是佔空間。test-first 社群有句名言:「永遠不要信任一個你沒見過失敗的測試。」

刪除 Pessimistic Tests(悲觀測試)#

一個永遠是紅燈的測試同樣沒有意義。對失敗測試的容忍度應該是,否則會產生警報疲勞,讓我們錯過真正重要的失敗。

修復或刪除 Flaky Tests(不穩定測試)#

時好時壞的測試和前兩者有同樣的問題——它們不會引發任何行動,頂多讓人多跑幾次測試。測試必須在紅燈時引發行動,做不到就不該留在程式碼庫中。

重構程式碼而非測試#

如果測試需要複雜的 setup 或大量重構,問題往往出在被測試的程式碼架構不良。重構的心力應該投入在程式碼,而非測試。如果測試比程式碼還複雜,誰錯了都分不清。

加速慢速測試#

端到端測試有其價值,但若慢速測試影響了其他測試的執行頻率,就需要處理:將慢速測試與快速測試分開,或觀察慢速測試實際捕捉到什麼錯誤——如果答案是「沒有」,它就是樂觀測試,可以移除。

Figure 9.5: How to deal with automated tests

處理配置碼#

硬編碼不好,但可配置性也有代價:每增加一個配置點就增加程式碼複雜度,而且通常會讓測試空間指數級增長

按時間範圍分類配置#

實驗性配置(Experimental)

包括 feature flag 和 A/B 測試。這些應在六週內移除。常見的問題是實驗性配置「洩漏」成永久配置,分裂了使用者群體卻只增加了複雜度。

過渡性配置(Transitional)

用於大規模變更期間(如從 legacy 系統遷移)。可以超過六週,但應利用兩個特性:

  • 對使用者通常是透明的,因此可以放在程式碼中集中管理
  • 通常有一個明確的結束點,可以用 strangler fig pattern 做 gate,等 gate 清空就能整體刪除

永久性配置(Permanent)

應該非常嚴格地審核。只有滿足以下條件之一才值得保留:

  • 增加使用量(如用配置旗標服務不同客戶,可能翻倍使用者數)
  • 維護成本極低(如 light/dark mode,只影響最外層樣式)

Figure 9.6: How to deal with configuration code

處理第三方套件#

第三方函式庫能快速獲得大量功能,尤其在安全前端框架方面,專業團隊的品質通常遠超自行開發。但代價是:

代價說明
更新成本需要調整自己的程式碼
認知負擔團隊必須了解函式庫的用法
不可預測性無法預知更新時機、功能被棄用、bug 被引入
安全風險函式庫是潛在的攻擊載體,且依賴的依賴也是風險來源

降低對外部函式庫的依賴#

  • 選擇高品質供應商的函式庫,減少破壞性變更
  • 頻繁更新:如果某件事很痛苦,就更常做——每次的變更量更小、風險更低
  • 將依賴分類為 enhancing(增強型)和 critical(關鍵型):
    • 增強型壞了 → 先移除,讓應用正常運作,之後再找替代
    • 關鍵型 → 謹慎評估是否真的必要
  • 如果只用了 jQuery 的一個 Ajax 功能,找一個更精簡的函式庫或自行實作可能更划算
  • 未使用的函式庫一律移除

Figure 9.7: How to deal with third-party libraries

處理可運作的功能#

程式碼是負債,使用量是用來支付這筆負債的價值。功能越多不等於價值越高——如果一個功能的維護成本大於其帶來的使用量增長,刪除它反而會讓程式碼庫更有價值。

提升價值有兩種途徑:增加收益或降低成本。鑑於功能的收益難以衡量,從降低成本下手(重構或刪除程式碼)通常更簡單有效。

任何未被使用的東西,無論其潛力有多大,都只是一筆開支。這就是為什麼你應該愛上刪除程式碼——每一次刪除,都立即讓程式碼庫變得更有價值。

Figure 9.8: How to deal with working features