架構分解#

架構模組化解釋了為什麼(why)要拆分單體應用,架構分解則描述了如何(how)拆分。拆解大型複雜的單體應用是一項耗時且困難的工作,因此首先必須判斷是否可行以及該採用何種方法。

避免 Elephant Migration Anti-Pattern——一次拆一塊(one bite at a time)看似合理,但往往導致一個沒有結構的分散式泥球(distributed monolith)。應該採取整體性的方法論。

Is the Codebase Decomposable?#

Figure 4.1: The decision tree for selecting a decomposition approach

在開始分解之前,架構師必須先評估程式碼庫是否具備足夠的內部結構來進行分解。若程式碼庫是一個 Big Ball of Mud(泥球架構),缺乏內部結構,則需要不同的處理策略。

Afferent and Efferent Coupling#

1979 年 Edward Yourdon 和 Larry Constantine 在 Structured Design 中定義了兩個關鍵的耦合度量:

  • Afferent coupling(傳入耦合):指向某個程式碼元件的傳入連接數量
  • Efferent coupling(傳出耦合):某個程式碼元件的傳出連接數量

在拆解單體架構時,這兩個度量特別有價值。例如,架構師會發現像 Address 這類共享類別被大量元件依賴——在拆分時必須決定如何處理這些共用資產。

大多數平台都有工具可以分析程式碼的耦合特性,提供類別和元件關係的矩陣視圖(如 JDepend)。

Figure 4.2: JDepend in Eclipse analysis view of coupling relationships

Abstractness and Instability#

Robert Martin 提出的兩個衡量程式碼庫內部特性的指標:

  • Abstractness(抽象度):抽象元素(abstract classes、interfaces 等)與具體元素(implementation classes)的比率
    • 公式:A = Sum(m^a) / (Sum(m^c) + Sum(m^a))
    • 反映程式碼中抽象實作的平衡
  • Instability(不穩定度):傳出耦合占總耦合(傳出 + 傳入)的比率
    • 公式:I = C^e / (C^e + C^a)
    • 高不穩定度的程式碼在被修改時更容易影響其他部分
    • I 接近 1 表示高度不穩定;接近 0 可能是 stable(多為抽象元素)或 rigid(多為具體元素)

Distance from the Main Sequence#

距主序線的距離是基於 abstractness 和 instability 的綜合指標:

  • 公式:D = |A + I - 1|
  • 在 abstractness (A) 與 instability (I) 的座標圖中,存在一條理想化的主序線(main sequence)
  • 元件越接近主序線,表示抽象度與不穩定度之間的平衡越好
  • 偏離主序線的兩個危險區域:
    • Zone of uselessness(無用區,右上角):過度抽象,難以使用
    • Zone of pain(痛苦區,左下角):過多實作、不夠抽象,脆弱且難以維護

Figure 4.3: Normalized distance from the main sequence for a particular component

如果一個程式碼庫中大多數元件都落在主序線附近,表示內部結構良好,適合進行分解。若大量元件落在無用區或痛苦區,則在嘗試重構前應先改善內部結構。

Component-Based Decomposition#

元件式分解是一種漸進式的提取方法,透過重構模式來精煉和提取單體應用中的元件(component),最終形成分散式架構。

  • Component 的定義:應用程式的一個建構單元,具有明確定義的角色、職責和操作集合,通常透過命名空間或目錄結構實現

Figure 4.5: The directory structure of a codebase becomes the namespace

  • 目標是從元件出發建立服務,而從個別類別出發

將單體應用遷移為分散式架構時,應以元件為單位建構服務,而非以個別類別為單位。

Component-based decomposition 的遷移路徑:

  • 通常先遷移至 service-based architecture(粗粒度的領域服務),可作為最終目標或通往微服務的跳板
  • 優勢:
    • 允許架構師判斷哪些領域需要更細的微服務粒度,哪些可保持粗粒度
    • 不需要拆分資料庫——可先專注於領域和功能分區
    • 不需要容器化或運營自動化——可使用與原應用相同的部署方式
    • 屬於技術性遷移,不涉及組織結構或測試環境的變更

Figure 4.6: Extracting a part of a system

Figure 4.7: Deleting what's not wanted is another way to isolate parts of a system

適用條件#

  • 程式碼庫具有可辨識的元件邊界和良好的內部結構

Tactical Forking#

戰術分叉是由 Fausto De La Torre 命名的一種務實方法,特別適用於程式碼庫是大泥球(big ball of mud)且缺乏內部結構的情況。

核心概念#

  • 傳統思維是提取(extraction)需要的部分——但在高耦合的程式碼庫中,提取一部分會拉出大量依賴
  • 戰術分叉的思路相反:複製整個程式碼庫,然後刪除不需要的部分
  • 刪除不需要的程式碼比提取需要的程式碼更容易——可以透過編譯和簡單測試來驗證

步驟#

Figure 4.8: Before restructuring, a monolith includes several parts

  1. 複製(clone)整個單體應用給每個團隊

Figure 4.9: Step one clones the monolith

  1. 每個團隊開始刪除不屬於其負責領域的程式碼

Figure 4.10: Teams constantly refactor to remove unwanted code

  1. 逐步隔離目標功能,持續清理不需要的部分
  2. 最終產出多個粗粒度的服務

Figure 4.11: The end state of tactical forking features two services

Trade-Offs#

優點:

  • 團隊可以立即開始,幾乎不需要前期分析
  • 開發者更容易刪除程式碼而非提取——高耦合程式碼的提取會引發連鎖依賴問題

缺點:

  • 產出的服務可能仍包含大量殘留的無用程式碼
  • 除非額外清理,新服務內部的程式碼品質不會比原來的泥球好——只是少了一些
  • 不同服務間的共用程式碼和元件可能出現命名不一致的問題

Tactical forking 的名稱恰如其分——它提供的是戰術性而非戰略性的重構方法,適合需要快速遷移重要或關鍵系統的場景。

Sysops Squad Saga: Choosing a Decomposition Approach#

Addison 和 Austen 使用 abstractness 和 instability 指標分析了 Sysops Squad 應用,發現大部分程式碼沿著主序線分布,確認程式碼庫適合分解

最終選擇了 component-based decomposition,原因如下:

  • 應用已具有良好定義的元件邊界,適合此方法
  • 減少每個服務中保留重複程式碼的可能性
  • 面對可靠性、可用性、可擴展性和工作流方面的問題,元件式分解提供更安全且可控的漸進遷移
  • 服務定義會透過元件分組自然浮現,無需預先知道要建立多少個分叉應用
  • 允許團隊協作識別共享功能、元件邊界和領域邊界

代價:遷移時間可能比戰術分叉更長,但團隊認為這個 trade-off 是值得的。

Addison 撰寫了一份 ADR 記錄此決定,確認採用 component-based decomposition 來遷移 Sysops Squad 單體應用到分散式架構。