The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

《先整理一下?個人層面的軟體設計考量》讀後心得分享

最近看了一本書 Tidy First?: A Personal Exercise in Empirical Software Design,作者是令人景仰的 Kent Beck 大神,他是一位擁有 Wikipedia 頁面的人物,也是美國著名的軟體工程師和作家,在軟體工程領域有著深遠的影響力。這本書整理了 32 個「整理程式碼」的心法,他並不是一本強調 Clean Code 或 Refactoring 的技巧,而是一些更細微的、個人層面的軟體設計考量,這些技巧都是作者在多年軟體開發經驗中的心得,也是他在日常開發中的一些習慣,我真的越看越亢奮,因為很多內容其實也跟我二十多年的開發經驗相吻合,有種為什麼不早點看到這本書的感覺,我特別整理了這本書的讀後心得,希望大家可以從中獲益。這本書在台灣已有翻譯書,名為 先整理一下?|個人層面的軟體設計考量,大家有興趣也可以買來看看。

Image

1. 保護類別 (Guard Classes)

  • 核心概念

    • 透過「提早退出」的策略 (如 if not condition: return),在主要邏輯開始前就先檢查排除不符合條件的情況。
    • 寧可在程式最前面就中止,也不要把錯誤或不符條件的狀況一路帶到後面。
  • 細部說明

    • 避免巢狀 if 不斷向下縮排,直接在條件不符合時立即 returnthrow (拋出例外)。
    • 若要檢查的保護條件過多,可以考慮額外拉出檢驗層 (validation layer) 或使用專門的函式庫,減少主要邏輯的混亂。
    • 例如在後端框架中常見的做法:在 controller 或 middleware 設置大量驗證,若失敗就立刻返回錯誤。
  • 讀後心得

    • 先處理前置檢查能讓主要邏輯更集中,也提高可讀性。
    • 如果發現保護條件非常複雜,代表需求或業務規則可能過度分散,需要進一步的架構抽象。

2. 無用程式碼 (Dead code)

  • 核心概念

    • 刪除不會再被呼叫或已經失去意義的程式碼,讓系統保持整潔。
    • 此舉不僅是風格問題,更是避免團隊成員浪費時間理解或誤用無效程式碼。
  • 細部說明

    • 「也許之後會用到」通常是錯覺,可透過版本控制系統保留歷史紀錄即可。
    • 要刪除時,可每次只移除少量,便於後續回溯;大規模刪除也要先確定沒其他模組或專案依賴。
  • 讀後心得

    • 保持精簡能提升整體可讀性與維護效率,降低專案「技術債」(Technical Debt)。
    • 這也反映 YAGNI (You Aren't Gonna Need It) 原則,避免過度設想未來需求。

3. 正規化對稱性 (Normalize symmetries)

  • 核心概念

    • 在程式撰寫時盡可能使用統一的風格、結構或命名方式,不要讓同一功能的程式碼有多種寫法並存。
  • 細部說明

    • 例如在 JavaScript/TypeScript 中,可統一使用 function expressionfunction declaration,而非混雜使用,在命名上也遵守團隊約定。
    • 工具 (如 Linter、Formatter) 只能解決部分問題,更多時候需要團隊明確定義規範並持續在 Code Review 中推行。
  • 讀後心得

    • 一致性的好處在於「讓讀者(維護人員)能預測接下來會怎麼寫」,減少理解成本。
    • 風格多變會讓新同事或跨專案開發者感到困惑,也增加 bug 機率。

4. 新介面,舊實作 (New Interface, Old implementation)

  • 核心概念

    • 當原介面過時或難用時,可以先建立「新介面」並在其中呼叫舊邏輯,作為過渡方式。
  • 細部說明

    • 這類作法類似 Adapter Pattern,讓使用者開始改用新介面,內部暫時維持呼叫舊的實作。
    • 若要同時保留舊介面做 A/B 測試或漸進式切換,需要注意相容性、測試是否仍能通過。
  • 讀後心得

    • 漸進式替換在大型專案中常見,能減少全面升級的風險,也避免大量一次性重構帶來的衝擊。
    • 後續可觀察用戶使用狀況,再逐步移除舊介面或做最終收斂。
    • 例如你想將類別庫從同步的寫法改為非同步方法,可以僅修改介面並加上 async Task 即可,現有的程式邏輯都不變,這樣可以確保程式的穩定性。

5. 讀取順序 (Reading Order)

  • 核心概念

    • 調整程式碼與檔案的結構,使人能按「自然理解路線」去閱讀,無須四處跳行。
  • 細部說明

    • 可將核心或高階邏輯放前面,細節或輔助函式放後面;大型專案可按執行步驟或功能模組分資料夾。
    • 若是有多個檔案並且預先定義好的執行流程,可在檔名或資料夾名稱前加數字,例如 01-Setup, 02-Validation 等,指示執行順序。
  • 讀後心得

    • 大部分開發者花最多時間在閱讀程式碼,好的結構能大幅減少溝通與維護障礙。
    • 當專案規模變大,合適的命名與排序比起雜亂更能降低新進人員的學習曲線。
    • 讓程式碼貼近真實的需求,可以增加程式的可讀性,也更容易對齊概念。

6. 內聚順序 (Cohesion Order)

  • 核心概念

    • 將耦合度高或經常同時修改的程式碼集中放在一起,提高「內聚度」。
  • 細部說明

    • 若檔案之間關係非常密切,可置於相同資料夾;同一類別裡的相關方法也靠近放置,避免到處散落。
    • 若耦合無法在當下根治 (因大範圍的重構需求高),就以「集中管理」的方式先降低維護負擔。
  • 讀後心得

    • 內聚度高可以使修改特定功能時,更容易掌握整個變動範圍。
    • 反之若拆得過度分散,只會徒增開發者來回切換檔案的困擾。

7. 將宣告和初始化一起移動 (Move Declaration and Initialization Together)

  • 核心概念

    • 變數的宣告與初始化盡量放在同個位置,減少讀者在程式中找尋「此變數何時被賦值」的混亂。
  • 細部說明

    • 若某分支才需要該變數,則在分支內宣告即可,不必在外層「空宣告」。
  • 讀後心得

    • 這是微小但有效的可讀性提升,養成習慣後能讓程式邏輯更線性。
    • 也避免了空變數帶來的可能誤用風險或意外覆蓋。
    • 能用 const (不可重新賦值) 就不用 let

8. 解釋變數 (Explaining Variables)

  • 核心概念

    • 面對複雜運算式或深層呼叫,可先將部分邏輯萃取為具意圖的變數,提高可讀性。
  • 細部說明

    • 例如:

      const result = someLongCondition(a, b) && anotherCheck(c, d);
      

      可拆成:

      const isValidA = someLongCondition(a, b);
      const isValidB = anotherCheck(c, d);
      const result = isValidA && isValidB;
      
    • 透過命名解釋該運算背後邏輯,而非全都塞在同一行。

  • 讀後心得

    • 命名即可當作「註解」,讓後來維護者快速了解該段運算的用途或背景。
    • 小心別過度拆到每個小步都變成變數,反而增加閱讀負擔。

9. 說明常數 (Explaining Constants)

  • 核心概念

    • 避免「魔幻數字」(Magic Number) 直接散布在程式碼中,改以明確命名的常數或枚舉表示。
  • 細部說明

    例如:

    if (req.ConcurrentReq > 10) {
      // ...
    };
    

    可以將 10 取代為一個有明確意義的常數名稱:

    const MAX_CONCURRENT_REQ = 10;
    if (req.ConcurrentReq > MAX_CONCURRENT_REQ) {
      // ...
    };
    
    • 若是錯誤碼或狀態碼,也可考慮使用枚舉 (Enum),讓語意更清楚。
  • 讀後心得

    • 每個數字後面都可能隱含規則或業務假設,替它取一個好名字,可在維護時防止誤解。
    • 也讓後續改動該數值時更輕鬆,不用在程式裡地毯式搜尋。

10. 明確的參數 (Explicit parameters)

  • 核心概念

    • 函式或方法的傳入參數應盡量清楚,避免直接用 Map 或大範圍物件使人搞不清楚哪些欄位被使用。
  • 細部說明

    • 若參數太多,可以將函式分割。上層函式負責收集參數並明確地將它們傳遞給下層函式。

      例如,如果你看到這段程式碼:

      params = { a: 1, b: 2 }
      foo(params)
      
      function foo(params)
          ...params.a... ...params.b...
      

      可以通過將 foo 分割成兩個函式來使參數變得更明確:

      function foo(params)
          foo_body(params.a, params.b)
      
      function foo_body(a, b)
        ...a... ...b...
      
    • 另一個需要明確參數的情況是,當你發現程式碼深處使用了環境變數。將參數明確化,讓呼叫函式的地方先取得環境變數再傳入,這樣會使你的程式碼更易於閱讀、測試和分析。

  • 讀後心得

    • 可以使用「物件參數 + 型別定義」的形式,在 TS/JS 中常用解構賦值。
    • 當參數在 2~3 個以上時,考慮改為物件參數能提升可讀性,也減少位置錯誤。
    • 對呼叫端來說,顯示指定哪些參數能避免混淆,也方便 IDE 或編譯器進行提示。
    • 參數明確化是提升介面易用度的關鍵。

11. 區塊聲明 (Chunk Statements)

  • 核心概念

    • 利用空白行或適度註解,將邏輯相關的程式碼分塊,讓人一眼就能辨別不同階段或功能。
  • 細部說明

    • 在長函式或複雜流程中,適度留白能明顯區隔各步驟;
    • 也可輔以 logger.info(...) 或標題註解,指示下面一行的程式要做什麼。
  • 讀後心得

    • 透過小手段就能有效提升程式閱讀的「段落感」。
    • 搭配其他重構 (如擷取輔助函式) 使用,更能增進可維護性。

12. 擷取輔助程式 (Extract helper)

  • 核心概念

    • 把一段獨立且用途明確的程式碼抽成「輔助函式」,以免主要邏輯過於臃腫或重複。
  • 細部說明

    • 可以單純為了重構而提取函式,先把易於出錯待改動的區域集中再進行調整。
    • 命名要能貼近功能描述,避免產生一堆 helper1, helper2 之類的模糊名稱。
  • 讀後心得

    • 共享邏輯不再散落各處,也方便後續修改或擴充,只需在輔助函式裡進行。
    • 此方式特別適用於表單驗證、共同檢查邏輯等情況。

13. 一堆 (One Pile)

  • 核心概念

    • 過度拆分會導致理解困難,有時「合併回單一塊」反而更易於閱讀與掌握全貌。
  • 細部說明

    • 若小函式彼此間頻繁交互、或需要許多共享變數,合併成一個較大區塊後再思考更適當的重構方式。
    • 在測試中也常遇到:測試資料最好放在同一處明顯位置,以免到處跳。
  • 讀後心得

    • 「小就是好」並非絕對,若拆得太零碎,可能造成引數狂飆、上下文難以追蹤。
    • 先合併「回歸原塊」再決定是否需要精細切分,能避免過度抽象帶來的混亂。

14. 解釋註解 (Explaining comments)

  • 核心概念

    • 適當撰寫註解,補充程式碼本身無法顯示的背景、原因、或當下尚未解決的疑慮。
  • 細部說明

    • 只說明「程式已經做什麼」的註解沒太大意義,重點在「為什麼要這樣做」。
    • 發現缺陷或可能存在問題時,在註解中標示 TODOFIXME,協助日後維護。
  • 讀後心得

    • 要透過程式碼來反推原始的需求或設計,幾乎不可能,適當的註解有助於理解程式。
    • 註解能降低後來閱讀者重複摸索的時間,也能保存當時的設計思路。
    • 不用擔心「註解太多」,只要確保內容有價值即可。

15. 刪除冗餘的註解 (Delete redundant comments)

  • 核心概念

    • 與程式碼內容重複或無需存在的註解,應果斷移除,維持註解區域的純粹性。
  • 細部說明

    • 如果程式碼自我解釋就足夠 (透過好命名或直接可讀的結構),不必額外再寫同義註解。
    • 要著重在程式碼「說不出的」那些事,例如背後業務限制或程式設計考量。
  • 讀後心得

    • 留下來的註解越精煉,越能成為維護者寶貴的資訊來源。
    • 過多無用註解會讓真正關鍵的註解被淹沒,也增加維護成本。

16. 分開整理 (Separate Tidying)

  • 核心概念

    • 程式碼整理應與功能或行為改動分開在不同 PR 或 Commit 中,以便審查與追蹤。
  • 細部說明

    • 若將整理和行為變更混在一起,Review 者難以分清哪部分是重構、哪部分是新需求。
    • 建議小規模、多次提交,便於日後回溯與降低合併衝突。
  • 讀後心得

    • 整理程式碼和增修功能分離,能保證你的 Repo 健康,也提高團隊協作效率。
    • 在高頻率開發環境下,此原則能避免合併混亂與意外破壞行為。

17. 逐步整理 (Chaining)

  • 核心概念

    • 小幅度重構常引發更多「需要做的重構」,彼此連鎖形成一系列微整理。
  • 細部說明

    • 程式碼整理 (Tidying) 是一個循序漸進的過程,從小步驟開始,逐步達成更大的結構變化。
    • 例如:先引入 Guard Classes → 發現該條件可再用輔助函式 → 又衍生加強命名規範 →… 形成串接。
    • 適當的整理能夠提升程式碼可讀性和維護性,但過度整理可能會分散注意力,導致未完成實際變更。
  • 讀後心得

    • 一次性大規模重構風險高,漸進式小步驟更容易掌控。
    • 這種「連鎖反應」在實務相當常見,一步整理就揭示下一步潛在改進。
    • 注意不要無止盡擴散,需掌握優先次序及風險。

18. 批量大小 (Batch Sizes)

  • 核心概念

    • 每次重構的改動量若過大,容易產生 Review 困難、合併衝突;若太小,又可能造成過多的提交與流程負擔。
  • 細部說明

    • 視專案與團隊規模選擇合適批量。
    • 若團隊能快速 Review,小批量有效;反之可稍微放大批量以減少合併次數。
    • 調整行為 (功能) 改動的批量也是相同邏輯,盡量避免「一次改一千行」的巨大提交。
  • 讀後心得

    • 找到「適度批量」能兼顧快速開發與降低衝突,這需要團隊文化與工具配合。
    • 盡量讓改動保持在可理解範圍內,就能讓專案持續推進。

19. 節奏 (Rhythm)

  • 核心概念

    • 小幅度重構一般只需要幾分鐘到一小時,若花太久,代表範圍或設計狀況可能過度複雜。
  • 細部說明

    • 當「先整理一下」經常超過一兩小時,表示已跨入大規模重構領域,需要更完善的計畫。
    • 建議維持「多次、小步、短時間」的節奏,確保每次都能快速驗證與回饋。
  • 讀後心得

    • 簡單整理往往能帶來即時好處;若老是陷入漫長的重構,效益可能被迫打折扣。
    • 高頻短期的 "tidy" (整理程式碼) 有助於不斷維持程式碼品質,避免積累成日後沉重的技術債。

20. 理順紛亂 (Getting Untangled)

  • 核心概念

    在程式碼變更過程中,整理和行為變更往往會交織在一起,可能導致難以分開處理。

  • 細部說明

    面對這種情況時,有三個選項可以選擇:

    1. 直接提交變更 (快速但可能混亂)
    2. 拆成多個 PR 處理 (較為禮貌,但需要時間)
    3. 放棄目前的進度重新整理 (最完整,但最耗時)

    這些選擇各有利弊,最終的選擇應該考慮到程式碼的可讀性和未來維護的需求。

  • 讀後心得

    • 有時重新實作可看見更多優化,但需投入更大成本。
    • 若時間壓力大,先完成 MVP (Minimum Viable Product) 並註解問題點,再擇期細修。
    • 這三種選擇無絕對對錯,端看當前時程、專案重要性、以及團隊資源而定。
    • 善用版本控制與分支策略,可大幅降低「打掉重練」或多條件並存的風險。

21. 首先、之後、後來、從不 (First, After, Later, Never)

  • 核心概念

    • 不同程式碼整理有不同優先級:NeverLaterAfterFirst
  • 細部說明

    • Never:若此段程式碼不再變動或完全不具學習價值,就不必花力氣。
    • Later:如果暫時沒時間或收益不明顯,可等功能需求逼近時再動手。
    • After:功能改完後,立即整理掉臭味(小 hack),讓後續更好維護。
    • First:若提早整理能為後續改動帶來大幅收益或更明確邏輯,就應先做。
  • 讀後心得

    • 這是對「何時該整理」的思考模式,彈性地面對各種開發情境。
    • 清楚分類也能避免漫無邊際地重構。

22. 有益的關聯元素 (Beneficially Relating Elements)

  • 核心概念

    • 以「程式元素(elements)與它們之間的關係」來思考軟體設計,盡力維持對系統有益的互動,而非盲目堆疊功能。
  • 細部說明

    軟體設計是一個將程式元素有效連結並創造互惠關係的過程。

    程式元素可能是變數、函式、類別、模組,或整個子系統;常見關係有呼叫 (invoke)、參考 (refer)、監聽 (listen) 等。

    這不僅僅是對程式碼的組織,更關乎如何通過設計使程式碼中的元素互相協作,從而達成更高效、更易維護的程式系統。

    • 元素:軟體系統由多個不同層級的元素組成,從最基本的元素 (如: tokens) 到複雜的系統結構 (如物件或模組)。每個元素都有明確的邊界,並且可以包含其他子元素,形成層級結構。

    • 關聯:軟體中的元素之間存在各種關聯,例如函數呼叫、資料引用等。這些關聯定義了元素之間的互動方式。

    • 互惠關係:良好的設計使得元素之間的關聯能夠帶來實質的好處。例如,當一個函數簡化了另一個函數的計算過程,則兩者之間的關聯對整體設計有益。設計師的工作就是創造這樣的關聯,並在設計中創建新的元素和關聯,以提升整體效益。

    在設計或重構時,我們可選擇建立、刪除元素,或改變關係,以創造更高的可維護性與靈活度。

  • 讀後心得

    • 軟體設計不僅僅是撰寫程式碼,更是一種對程式碼結構和元素間關聯的精心設計。
    • 良好的軟體設計關鍵在於如何有效地管理元素之間的關係,使得每個元素都能在保持簡潔的同時,最大化其功能的互惠效益。
    • 這不僅使程式碼更易理解,也為未來的變更和維護提供了更大的彈性。
    • 這種以關係為中心的思考,能幫助我們判斷哪些元素該放在一起、哪些該隔離。
    • 也是微重構時可以快速檢視「這些函式或類別對系統的貢獻與影響」。

23. 結構與行為 (Structure and Behavior)

  • 核心概念

    • 軟體價值來自兩方面:「現在能做什麼」(行為) 與「未來可以做什麼」(結構所帶來的彈性)。
  • 細部說明

    • 結構變更行為變更雖然都是創造價值的方式,但它們在可逆性上有根本的區別。
    • 行為變更:如新增一個按鈕,容易被識別和衡量。
    • 結構變更:較為抽象,難以直接看出其效果或是否正確。
    • 好的結構能快速因應新需求或市場變動,壞的結構則會逐漸讓維護與擴充變得昂貴。
    • 在商業考量下,也必須在「立即盈利 (上線新功能) 」和「持續改良結構」之間取得平衡。
  • 讀後心得

    • 「先整理一下」對當下功能可能沒有直接收益,卻能避免日後付出更高的維護成本。
    • 透過持續的重構,讓系統隨時保有成長與變更的彈性。
    • 行為變更直接且容易量化,結構變更則隱性且不容易評估效果。
    • 這讓我們在進行結構性改動時,更需要謹慎和計劃,因為它們的效果並不如行為變更那樣明顯且可立即觀察到。
    • 要解答這些問題,我們需要了解結構變更的可逆性,並根據實際情況做出合理的判斷。

24. 經濟學:時間價值與選擇性 (Economics: Time Value and Optionality)

  • 核心概念

    • 「今天的一美元比明天的一美元更值錢」及「保留未來更多選擇權」是兩大經濟概念,對軟體開發同樣適用。
  • 細部說明

    本章提出兩個關鍵觀念:

    1. 時間價值 (即今天的錢比明天的錢更有價值)
    2. 選擇權 (在不確定情況下,擁有選擇比擁有具體物品更有價值)。

    這些觀念不僅影響財務管理,也對軟體設計開發策略有深遠的影響。

    • 時間價值:今天擁有的金錢比明天擁有的金錢更有價值,這意味著應該儘早賺取並延後支出。這與軟體設計中的即時反應和快速執行策略相呼應。
    • 選擇權:在面對不確定性時,選擇權比具體的物品更為重要。這體現在創建選項以便未來可以根據情況作出調整,這與軟體設計中的靈活性和可擴展性有關。
    • 衝突:這兩者有時會相互衝突,因為立即賺錢可能會減少未來的選擇,而不賺錢又可能無法利用未來的機會。
  • 讀後心得

    • 盡快上線功能可及早獲利,但若程式結構糟糕,後續維護成本可能爆炸;
    • 所以必須同時考量「提早收益」與「保留系統選擇權」的平衡。
    • 技術債往往是快速上線後的副作用,在某些專案初期或能接受,但長期看仍要重工 (重新加工)。
    • 保留選擇權可避免被未來變動逼到牆角,成為高昂的技術負債。

25. 今天的一美元 > 明天的一美元 (A Dollar Today > A Dollar Tomorrow)

  • 核心概念

    • 因為不確定性時間成本,現在拿到的錢比未來更有價值。
  • 細部說明

    金錢的價值不僅取決於它的數量,還取決於何時獲得、是否能夠投資以及相關風險。這與軟體設計中的即時變更和未來可擴展性選項的關聯相似。

    • 在評估專案時,資金早點到手就能投資或降低風險;
    • 但若「先整理」能大幅降低日後開發成本,短期放棄一些時程也可能值得。
  • 讀後心得

    • 軟體設計不僅是關注功能的實現,也需要考慮到設計變更的時間成本。
    • 不可只用短視角度追求「馬上賺錢」,否則可能導致系統品質嚴重惡化,未來需要更大代價補救。不同專案會有不同取捨,需具體分析。
    • 理解時間價值能幫助我們做出更具遠見的決策,例如,如何在進行行為變更時,選擇適當的時機進行程式碼整理,以獲得最大的長期效益。

26. 選擇權 (Options)

  • 核心概念

    • 軟體的價值不只來自於當下能做什麼,也來自於「未來擴充或變更的潛能」。
  • 細部說明

    • 軟體設計不僅要關注當前行為的實現,還需要專注於創造未來可能實現的選擇,這樣能更好地應對不確定性並提升軟體的靈活性。
    • 將金融中的選擇權(options)概念引入軟體設計,指出在設計過程中,創造選擇的價值往往超過立即實現行為變更的價值。
    • 選擇權概念:選擇權是指擁有在未來某個時間以固定價格購買某物的權利,而非義務。在軟體設計中的"選擇權"意味著今天的設計工作所付出的成本 (相當於選擇權的溢價),為未來的行為變更提供選擇的靈活性。
    • 未來選擇的價值:就像金融市場中,選擇權的價值取決於未來市場的不確定性,軟體設計中的選擇權也更有價值當未來的不確定性較大。這意味著,在設計階段,保留靈活性並不立即實現行為變更,往往比當前就決定並實現所有變更更具價值。
    • 設計與選擇權的平衡:設計過程中的選擇權與行為變更的平衡是關鍵。在某些情況下,越能創造更多未來可選擇的行為變更,越能提高系統的價值。與此同時,如何保持設計的簡潔性以便未來能夠輕鬆實現變更,也是成功的設計策略之一。
  • 讀後心得

    • 軟體設計不僅僅是對當前需求的解決方案,更是一種對未來需求的預備。
    • 將選擇權的概念引入軟體設計,讓我看到了設計中的另一層面:通過創造選擇而非立刻解決問題,可以更靈活地應對未來的變化與挑戰。這使得軟體設計更具策略性,能夠在面對不確定性時,提供更多的應對選項,而不僅僅是追求當前的行為變更。
    • 好的架構就像買保險,可快速因應新需求或市場變化;當需求尚未確定、狀況多變時,保留選擇權就顯得更重要。
    • 在變動劇烈的環境下,將系統設計得具彈性能帶來長遠收益。
    • 大企業常願投入預算在架構演進上,就是為了保持後續競爭力。

27. 選擇權 vs. 現金流 (Options Versus Cash Flows)

  • 核心概念

    • 從「現金流」角度,快速上線功能,盡早賺錢、延後支出;
    • 從「選擇權」角度,若能藉由整理換得後續彈性,應考慮先整理。
  • 細部說明

    • 現金流與選擇權:現金流模型鼓勵早期賺錢並延後支出,而選擇權模型則鼓勵現在花費一些資源以換取未來更多的選擇和潛在回報。這兩者在某些情況下可能互相衝突,尤其在軟體設計過程中,選擇是否先進行整理 (tidy first) 會影響長期的價值創造。
    • 何時先整理:當整理的成本加上整理後進行行為變更的成本小於直接進行行為變更的成本時,應該選擇先整理。這樣能提高後續行為變更的效益。但有時候,即使短期經濟上不太支持先整理,從長期看來,創造的選擇權價值會超過即時的支出。
    • 判斷力與經濟激勵:在這個過程中,經濟上的判斷力和自我激勵是非常重要的。即便如此,做出設計決策仍然是基於對未來價值和選擇的判斷,而非單純的數字計算。設計的過程也不僅是關於經濟,還涉及與自己以及團隊成員的關係管理。
  • 讀後心得

    • 在軟體設計中,經濟效益和選擇權的平衡是一個非常微妙的問題。先整理程式並非總是最佳選擇,但在某些情況下,當我們能夠創造更多未來的選擇時,這樣的投資是值得的。這樣的決策更多的是一種判斷能力的鍛煉,而不是依賴嚴格的經濟計算。理解這一點讓我在進行設計時,更能平衡短期和長期的目標,並根據實際情況靈活應對。
    • 若整理成本 + 整理後再改功能的成本 < 直接開發功能的成本,顯然就該先整理。
    • 在短期時程壓力下,也可採用更細膩的漸進式重構。
    • 沒有一種模型適合所有專案,需衡量團隊速度、專案規模與長期收益。
    • 即使花幾分鐘做小整理,也可累積成更健康的程式碼基礎。

28. 可逆的結構變更 (Reversible Structure Changes)

  • 核心概念

    • 多數結構重構是可逆的(可還原或回退),但行為改變通常難以回頭。
  • 細部說明

    • 在軟體設計中,我們應該對待可逆不可逆的決策有所區分,對可逆決策可以快速進行調整,而對不可逆決策則應該更加謹慎,慢慢思考和檢查,以避免帶來巨大的風險。
    • 可逆的決策:大多數軟體設計決策都是可逆的。例如,提取輔助函式後如果不滿意,可以輕鬆地將其內聯(Inline)回原處,這樣就像該輔助函式從未存在過一樣。對於這類可逆決策,錯誤的成本比較低,因此我們不需要過度投資時間來避免錯誤。
    • 不可逆決策:與此相對,某些設計決策是不可逆的,例如將某個功能提取為服務 (extract as a service)。這樣的決策如果錯誤,會導致很大的代價,並且很難回退。因此,對於這類決策,我們應該在實施之前進行仔細思考,甚至可以先實現一個原型來測試其可行性。
    • 在設計過程中,我們要考慮決策是否會在整個專案的程式碼 (code base) 中擴散。如果一個改動會在專案中大範圍傳播,那麼一旦錯誤發生,回退將變得非常困難。這時候可以通過逐步進行程式碼整理 (tidy) 來幫助緩解這種風險。
  • 讀後心得

    • 理解「可逆」與「不可逆」能幫助我們決定要不要先花力氣去整理。
    • 面對不可逆改動要格外謹慎,先做足測試或原型驗證。
    • 大多數小重構都屬可逆,可以更放膽嘗試。

29. 耦合 (Coupling)

  • 核心概念

    • 「耦合」代表某部分程式變更時,其他部分也必須跟著改;耦合越高,維護成本越高。
  • 細部說明

    • 耦合是軟體開發中成本增加的主要因素,因為高度耦合的系統會導致修改一個地方時需要做大量的連鎖修改,增加了維護和修改的難度。
    • 耦合有兩個關鍵特徵
      • 1-N: 一個元素可以與多個其他元素在同一變更上有所耦合。
      • Cascading: 一旦一個變更從一個元素傳遞到另一個元素,這個隨之而來的變更可能會觸發另一輪變更,而這些變更本身也可能引發更多的變更。在複雜系統中,這樣的 Cascading (級聯變更) 會導致預期之外的後果,這使得軟體變更的成本呈現冪律分佈
    • 分析耦合需要透過實際變更歷史來看,若兩個模組經常一起改動,就代表存在耦合。
    • 工具能自動偵測某些耦合 (如呼叫關係),但業務層面邏輯層面的耦合更需人工判斷。
  • 讀後心得

    • 「先整理」常目的是降耦合,讓後續維護時不必大範圍波及。
    • 高耦合系統長期會積累大量負債,處理起來更費力。

30. 康斯坦丁等價原則 (Constantine's Equivalence)

  • 核心概念

    • 軟體的成本變更的成本大致相同,而變更的成本主要來自於大規模的變更
    • 這些大規模變更通常由於系統中的高耦合所引起,當一個元素的變更需要修改多個相關元素時,變更的成本就會急劇上升。
  • 細部說明

    • 根據 Constantine's Equivalence 的理論,軟體總成本近似等於未來各種「變更」的成本之和;
    • 大規模變更最昂貴,而耦合往往是大變更成本的源頭,減少耦合能夠有效降低整體開發和維護的成本。
    • 軟體開發成本:軟體開發的成本不僅來自於建立階段,更多的是來自於後期的變更。
    • 變更成本的分佈:變更的成本呈現冪律分佈,少數幾個重大變更的成本遠遠超過大量的小變更。這與「冪律分佈」的特徵相符,即少數大事件 (如複雜的變更) 所造成的損害遠大於數量龐大的小事件。
    • 耦合的影響:耦合是造成軟體高成本的根本原因,當系統內的元素之間存在強耦合時,變更一個部分會引發大量的後續修改,這大大增加了維護的難度和成本。因此,降低耦合度對於降低軟體開發和維護成本至關重要。
    • 若耦合導致某小改動需要連帶修改多處,將累積形成高額成本。
    • 降低耦合能顯著減少未來重構或功能延伸的難度。
  • 讀後心得

    • 這提供了一個衡量角度:若你的系統很容易做出大改動,長期成本就相對低。反之,「一次改就牽一髮動全身」會成為專案的噩夢。

31. 耦合 vs. 解耦 (Coupling Versus Decoupling)

  • 核心概念

    • 不必完全消滅所有耦合;有時合理耦合能在短期內帶來效率或快速上線。
  • 細部說明

    • 耦合(coupling)與解耦(decoupling)之間的取捨是軟體設計中的一個重要決策。
    • 耦合的存在原因
      • 經濟上的選擇:在某些情況下,耦合是一個經濟上合理的選擇,因為它可以實現快速的行為變更,讓收入更早實現,支出則可以延後。這樣的耦合決策在當下是合適的,但隨著時間的推移,當需要更多變更時,耦合的成本就會顯現出來。
      • 變更需求的突發性:有時候,耦合的出現是不可預見的,當需求發生變化(例如需要支援新的語言版本或格式時),耦合才變得顯而易見。
      • 不可避免的耦合:某些耦合是系統內在的,無法完全避免。設計者需要對這些耦合進行合理的管理。
    • 解耦的過程
      • 在一些情況下,解耦可以通過引入接口(如通訊協定)來實現,這樣能夠將變更的成本集中在單一的地方,而不需要在多個地方做修改。然而,解耦不會完全消除耦合,因為有些變更仍然會需要在多個元件間同步進行。
      • 解耦的成本是動態的,且隨著時間推移會改變。在某些情況下,過度的解耦反而會帶來不必要的複雜性和成本。因此,減少耦合是有必要的,但並不是每一個細節都需要解耦。
    • 耦合與解耦的取捨
      • 軟體設計最終是一個權衡過程,在這個過程中,設計者必須平衡耦合的成本與解耦的成本。
      • 過度解耦可能帶來額外的開發負擔,而過多的耦合則會在後期帶來高昂的維護成本。
    • 在設計過程中
      • 耦合能夠簡化某些行為變更的實現,但隨之而來的代價是高昂的維護成本
      • 解耦能夠降低長期的維護成本,卻需要付出初期的高額開發成本
    • 設計師需要根據當前情境,選擇是承擔耦合的代價,還是進行解耦來創造未來的選擇權。
    • 解耦本身也需要支出成本,且過度抽象反而可能帶來新的複雜度或耦合。
    • 在設計時要在「付出解耦成本」和「維持耦合成本」之間權衡,看哪一種更符合需求。
  • 讀後心得

    • 耦合在某些情況下是有其必要性的,尤其是在早期開發過程中,它能夠讓系統變更更為快速和簡單。
    • 以適度解耦、提升可維護性為目標,確保系統靈活且不過度分裂。
    • 不要盲目推崇「全部拆乾淨」,不切實際且維護困難。

32. 內聚 (Cohesion)

  • 核心概念

    • 在軟體設計中,將具有相似職責或功能的元素放在一起,使它們緊密合作。
    • 高內聚的元素能夠簡化修改與分析,減少不必要的耦合,並提高系統的可維護性。
    • 內聚的目的是將耦合的元素組織在一起,並將不相關的元素移至其他地方。
  • 細部說明

    • 像單一責任原則 (SRP) 即是要提升內聚,讓類別或模組專注於一個領域任務。
    • 內聚的好處
      • 讓代碼更易於分析與理解。
      • 增加修改代碼的效率,因為修改高內聚的元素通常不會影響其他無關的部分。
      • 增強系統的穩定性,減少意外的行為改變。
    • 內聚的兩個要素
      • 將耦合的元素組織在一起:例如,若有多個函數彼此耦合,可以將它們抽取到一個子模組中,讓這些函數在該模組內保持高內聚。這樣的子模組使得功能相關的代碼更易於管理和維護。
      • 將不相關的元素移到其他地方:對於不耦合的函數,應該將它們移到與其功能更相關的地方,這樣能夠保持模組內的凝聚性,並讓每個模組有清晰的職責範圍。
    • 若發現有一群方法或屬性彼此強烈依賴,就應考慮獨立成更內聚的單位。例如:同一個命名空間、同一個類別、同一個方法等。
  • 讀後心得

    • 內聚指功能相關的元素應該被聚在一起,不相干的則分開,構成高內聚、低耦合的良好架構。
    • 無法內聚的功能可能預示著該需求邏輯本身就需要更上層重組 (需要更抽象的設計)。
    • 高內聚的模組易懂、易測試,也避免修改時牽涉太多區塊。
    • 在進行設計時,避免大幅度重組程式碼,而是應該逐步調整,將不相關的元素行動到更適合的位置,並確保每次修改都能讓程式碼變得更乾淨、更易於維護。這樣可以逐步提高程式碼的凝聚性,讓其在長期維護過程中更易於管理。

33. 結論 (Conclusion)

在軟體設計中,是否選擇「先整理一下」取決於多個因素,包括成本、收益、耦合度和凝聚力等。最重要的是,設計過程應該為自己和他人帶來更高的滿意度,並且要注意不要過度整理,以免影響到其他變更的進行。

本書的「先整理一下」強調的不是盲目地在做大規模重構,而是根據經濟、時機、耦合、內聚、個人心態等多方考量,靈活地做小步驟調整。

軟體設計不僅是技術層面的工作,更涉及到人際關係的管理。設計不僅要考慮如何改善系統的結構,還要考慮如何與團隊成員、業務相關方進行有效協作。理解「先整理一下」的真正意圖,能夠幫助我們在設計過程中做出平衡的決策,既不過度整理,也不忽略變更的需求。最終,這是一個關於如何在工作中實現平衡與合作的過程。

以結構與行為並重,關注軟體長期維護成本。

建議在開發過程中將「重構、整潔程式碼」視為常態,而非事情無可收拾時的最後手段。因為「先整理一下」(Tidy First) 往往能換來更好的可讀性與延展性。對個人與團隊而言,持續練習並內化這些原則,最終能大幅提升軟體品質與開發者幸福感。

總結

以上是針對《先整理一下?個人層面的軟體設計考量》書中 33 個章節的筆記。從各種微重構技巧,到軟體經濟觀點、耦合解耦議題,作者都透過短小卻實用的段落,提供了一套思考框架與實踐方式。

建議大家可以隨時將這些原則檢視應用於日常開發流程,使程式碼更易於閱讀與維護,並在滿足即時需求的同時,也保留充分的未來選擇權。若能持之以恆,就能逐步打造高內聚、低耦合、持續健康生長的軟體系統。

相關連結

留言評論