引言#
持續交付的核心要求是隨時保持應用程式處於可發佈狀態。但團隊經常面臨重大重構或複雜新功能的開發,這似乎不可能做到。分支(branching)看似解法,但作者認為這是錯誤的答案。本章描述如何在持續變更中維持可發佈狀態,其中關鍵技術之一是元件化(componentization)。
元件(Component) 的定義:一個應用程式內相當大規模的程式碼結構,具有定義明確的 API,可以被替換為其他實作。元件化系統的特徵是程式碼被劃分為離散的部分,透過定義良好、有限的互動與其他元件協作。
與之相反的是單體系統(Monolithic System)——沒有明確邊界或關注點分離,封裝性差,邏輯上獨立的結構之間緊密耦合,違反迪米特法則(Law of Demeter)。元件在不同平台上的表現形式各異:Windows 的 DLL、UNIX 的 SO 檔、Java 的 JAR 檔。
元件化設計不僅促進重用和良好架構(如鬆耦合),更是大型團隊高效協作最有效的方式之一。本章涵蓋三個自由度(度量空間):部署管線、分支、元件,以及如何在大型系統中同時管理它們。
保持應用程式可發佈#
持續整合(CI)和部署管線的前提是團隊在主線(mainline)上進行開發。但在開發過程中,團隊持續添加功能甚至進行重大架構變更,應用程式不一定可發佈。傳統做法是在發佈前進入穩定化階段,但這通常導致數週甚至數月才能發佈一次。
作者主張所有人都在主線上提交,而非使用分支。為此有四種策略:
- 隱藏未完成的功能
- 所有變更都以增量方式進行
- 使用抽象分支(Branch by Abstraction)進行大規模變更
- 使用元件解耦變更速率不同的部分
隱藏未完成的功能(Hide New Functionality)#
將新功能放入系統中,但讓使用者無法存取。例如,一個旅遊網站要新增飯店預訂服務,可以作為獨立元件在 /hotel URI 下開發,只要不開放該入口即可(可透過 Web 伺服器組態設定控制)。
實用建議: 可以透過組態設定(命令列選項、部署時或執行時組態)來開關功能。這種**功能開關(Feature Toggle)**機制在執行自動化測試時也非常有用。但要注意定期清理不再需要的開關選項,可使用靜態分析自動列出可用的組態選項。
將半完成的功能隨系統一起交付是好的實踐,因為:
- 始終在整合和測試完整系統
- 計畫和交付更容易,不需要引入依賴和整合階段
- 新元件從一開始就與其他軟體一起部署和測試
- 隨時檢測回歸問題
增量式變更(Make All Changes Incrementally)#
即使是大規模變更,也應拆解為一系列小的、可發佈的增量步驟。看似越需要分支的大變更,就越不應該分支。
增量方式的優點:
- 邊做邊解決保持應用程式運作的問題,避免最後的痛苦
- 隨時可以停下來,避免沉沒成本
- 額外的分析可以減少錯誤、產生更精準的變更
抽象分支(Branch by Abstraction)#
當變更太大而無法以增量方式進行時,可使用此模式替代版本控制中的分支:
- 在需要變更的部分上方建立抽象層
- 重構系統其餘部分使用該抽象層
- 建立新實作,在完成前不納入生產程式碼路徑
- 更新抽象層以委派給新實作
- 移除舊實作
- 若不再需要,移除抽象層
flowchart TD
A[1. 建立抽象層] --> B[2. 重構系統\n使用抽象層]
B --> C[3. 建立新實作]
C --> D[4. 更新抽象層\n委派給新實作]
D --> E[5. 移除舊實作]
E --> F[6. 移除抽象層\n直接使用新實作]補充說明: 抽象分支可以在非常高的層次運作(例如替換整個持久層),也可以在低層次運作(例如使用策略模式替換一個類別)。依賴注入(Dependency Injection)也是啟用抽象分支的機制之一。關鍵是找到或建立**接縫(seam)**來插入抽象層。
此模式也適用於將單體程式碼庫重構為模組化形式——使用外觀模式(Facade Pattern)隔離入口點,在建立新的模組化版本同時保持舊程式碼運作,有時被稱為「掃到地毯下(Sweeping It Under the Rug)」。
抽象分支最困難的兩個部分是隔離入口點和管理開發中功能的變更。但這些問題比分支更容易管理。若真的找不到好的接縫,可以先用分支將程式碼重構到可以進行抽象分支的狀態。
核心觀點: 無論使用哪種大規模變更技術,都極度需要一套完整的自動化驗收測試套件。單元測試和元件測試的粒度不夠細緻,無法在大幅變更時保護業務功能。
依賴(Dependencies)#
依賴發生在一段軟體需要另一段軟體才能建置或執行的情況。本章區分兩個重要面向:
元件 vs. 函式庫#
- 函式庫(Library):團隊不控制的第三方軟體套件,通常很少更新
- 元件(Component):應用程式依賴的、由自己的團隊或組織內其他團隊開發的軟體,通常頻繁更新
建置時 vs. 執行時依賴#
- 建置時依賴(Build-time Dependency):編譯和連結時必須存在(如 C/C++ 的標頭檔、Java 的介面 JAR)
- 執行時依賴(Runtime Dependency):應用程式執行時必須存在(如 DLL、SO、完整實作的 JAR)
依賴地獄(Dependency Hell)#
又稱「DLL 地獄」——應用程式依賴某個特定版本,但部署時使用了不同版本或根本沒有。
各平台的解決方案:
| 平台 | 解決方案 |
|---|---|
| .NET | 引入組件(Assembly)概念,透過全域組件快取(GAC)區分不同版本的函式庫 |
| Linux | 在 SO 檔名後附加整數版本號,使用軟連結決定系統預設版本;Debian 套件管理系統是最優秀的依賴管理工具之一 |
| Java | 原始 classloader 設計不允許同一 JVM 中存在多個版本的類別,OSGi 框架解決了此問題。缺乏 OSGi 時容易遇到菱形依賴問題(Diamond Dependency Problem)——應用程式依賴兩個函式庫,而這兩個函式庫依賴同一底層函式庫的不同版本 |
管理函式庫(Managing Libraries)#
兩種合理的方式:
方式一:簽入版本控制
- 建立
lib目錄,下分build、test、run子目錄 - 使用包含版本號的命名慣例(如
nunit-2.5.5.dll) - 優點:檢出即可重複建置
- 缺點:儲存庫可能變得龐大,跨專案的版本一致性難以手動管理
方式二:使用依賴管理工具(如 Maven、Ivy)
- 在專案組態中宣告所需的函式庫版本
- 工具自動下載、傳遞性解析依賴、確保依賴圖無不一致
- 首次建置較慢,後續會快取在本機
核心觀點: 關鍵約束是建置必須可重複——任何人在任何時間檢出專案並執行自動化建置,都必須得到完全相同的二進位檔。建議自行管理組織的 artifact repository(如 Artifactory、Nexus),以控制可用的函式庫版本、確保重複性並方便授權稽核。
元件(Components)#
如何劃分元件#
元件應具備以下特性:可重用、可替換(實作相同 API)、可獨立部署、封裝一組一致的行為和職責。
劃分為元件的好處:
- 將問題分解為更小且更具表達力的區塊
- 反映系統不同部分的不同變更速率和生命週期
- 鼓勵清晰的職責劃分,限制變更影響範圍
- 提供建置與部署流程的額外最佳化自由度
將元件從程式碼庫分離的好理由:
- 需要獨立部署某部分(如伺服器或富客戶端)
- 將單體轉為核心加外掛架構
- 元件提供對外系統的介面
- 編譯連結時間過長
- 在開發環境中開啟專案太慢
- 程式碼庫太大,單一團隊無法有效處理
常見陷阱: 不建議讓團隊各自負責單一元件。需求通常不會沿元件邊界劃分,按元件分團隊會增加不必要的溝通成本,且成員容易形成穀倉效應(silo)並進行局部優化。更好的做法是按功能領域(functional area)組織跨功能團隊,讓每個團隊接手一條 story 流,視需要觸及任何元件。確保每個人都有權變更程式碼庫的任何部分,定期輪調成員,並保持團隊間良好溝通。
元件化不等於 N-Tier 架構。元件化是將邏輯分離到封裝的模組中,而分層只是其中一種可能的組織方式。元件設計與分層是正交的——不應每層建立一個元件,一層內通常有多個元件,且元件可被多層使用。
最後值得注意康威定律(Conway’s Law):組織設計的系統會反映其溝通結構。小型、同地點的團隊傾向產出緊密耦合的系統;分散式團隊傾向產出模組化系統。
元件管線化(Pipelining Components)#
即使應用程式由多個元件組成,最簡單的做法是使用單一管線建置整個應用程式——每次提交時建置和測試所有東西。在大多數情況下,作者建議先用單一管線,直到回饋速度變得太慢。
適合分離管線的情境:
- 不同生命週期的部分(如數週才需重建的 OS 核心)
- 不同團隊負責的功能分離區域
- 使用不同技術或建置流程的元件
- 被多個專案共用的元件
- 相對穩定、不常變更的元件
- 單一建置時間過長(但此閾值比多數人認為的要晚得多)
每個元件或元件集的管線應包含:編譯、產生可部署二進位檔、執行單元測試、執行驗收測試、支援手動測試。二進位檔通過後發佈到 artifact repository。
補充說明: 作者強調不是每個 DLL 或 JAR 都要有自己的管線。一個元件可以包含多個二進位檔。指導原則是最小化管線數量——一個比兩個好,兩個比三個好,盡可能延後才拆分為平行管線。
整合管線(Integration Pipeline)#
整合管線以各元件管線的二進位輸出為起點:
- 第一階段:組合適當的二進位檔集合為可部署的套件
- 第二階段:部署到類生產環境並執行煙霧測試(smoke test)
- 後續階段:完整的驗收測試與常規部署階段

Figure 13.1: Integration pipeline
整合管線的兩大原則:快速回饋和建置狀態可見性。若管線鏈過長,可在二進位檔建立且單元測試通過後就觸發下游管線。必須能從整合建置追溯到各元件的版本,以快速找出破壞原因。
若多個元件在兩次整合管線執行之間都有變更,建置很可能經常失敗。最佳策略是建置所有可能的良好版本組合;次佳是盡可能頻繁地用各元件最新版本組裝應用程式。
管理依賴圖(Managing Dependency Graphs)#
版本化所有依賴至關重要。若不版本化,就無法重現建置、無法追溯破壞原因、無法找到函式庫的最後「良好」版本。
元件之間的依賴圖應該是有向無環圖(DAG, Directed Acyclic Graph)。若圖中有環(cycle),就是病態的依賴問題。

Figure 13.2: A dependency graph
建置依賴圖#
以一個投資組合管理應用為例:它依賴定價引擎、結算引擎和報表引擎,這些又都依賴一個框架,定價引擎還依賴第三方 CDS 定價函式庫。上游(upstream)指圖中更左邊的依賴,下游(downstream)指更右邊的依賴。
每個元件應有自己的管線,由其原始碼變更或上游依賴變更觸發。關鍵場景:
- 應用程式本身變更:只需重建應用程式
- 報表引擎變更:重建報表引擎,通過測試後用新版本重建應用程式
- 第三方函式庫更新:用新版本重建定價引擎,再重建應用程式
- 框架變更:重建所有直接下游元件,若全部通過則用新版本重建應用程式;若任一失敗,應用程式不應重建
最重要的約束:應用程式應只建置在一個版本的框架上,絕不能讓定價引擎用一個版本的框架、結算引擎用另一個版本——這就是建置時的菱形依賴問題。

Figure 13.3: Component pipeline
管線化依賴圖(Pipelining Dependency Graphs)#
為提高回饋速度,下游專案在上游專案的提交階段完成後即被觸發(不需等驗收測試通過),建立的二進位檔儲存在 artifact repository。除了部署到手動測試和生產環境外,所有觸發都是自動的。

Figure 13.4: Visualizing upstream dependencies

Figure 13.5: Visualizing downstream dependencies
CI 工具應確保一致的元件版本在整個管線中使用、防止依賴地獄,且讓團隊可追溯每次建置中使用的元件版本。
若需對元件進行破壞 API 的變更,可建立發佈分支(Release Branch):在分支上維護舊版本,在主線上開發新版本。下游使用者可繼續使用舊版本的二進位檔,準備好時再切換。

Figure 13.6: Branching components
何時觸發建置?#
理想上,上游依賴有任何變更就觸發新建置。但實務中存在張力:持續更新依賴可獲得最新功能和修復,但也可能花大量時間處理整合問題。
關鍵考量是信任程度:
- 自己團隊的元件:整合越頻繁越好,最好用單一建置
- 組織內其他團隊的元件:各自管線,根據變更頻率和回應速度決定是否跟進最新版本
- 第三方函式庫:控制力和可見性最低,應最保守——除非修復了你遇到的問題或版本已不再支援,否則不要盲目更新
謹慎樂觀(Cautious Optimism)#
Alex Chaffee 提出的策略,為依賴圖引入三種狀態:
| 狀態 | 說明 |
|---|---|
| Static(靜態) | 上游變更不觸發新建置 |
| Fluid(流動) | 上游變更總是觸發新建置 |
| Guarded(守護) | 原本是 fluid 但建置失敗後自動切換,鎖定已知良好版本,提醒團隊需要解決問題 |

Figure 13.7: Cautious optimism triggering
此策略自動退回任何因上游壞版本導致的建置失敗,確保應用程式始終「綠燈」。
另一種起點策略是知情悲觀(Informed Pessimism):所有觸發設為 static,但當上游有新版本可用時通知下游開發者。
補充說明: Apache Gump 是 Java 世界中最早的依賴管理工具之一,用於管理 Apache 專案間的版本依賴。其經驗教訓是:保持依賴圖淺層(shallow),並盡力確保向後相容性——在建置時對元件圖進行積極的回歸測試有助於達成此目標。
循環依賴(Circular Dependencies)#
最棘手的依賴問題。元件 A 依賴元件 B,而 B 又依賴 A,形成看似無法解決的自舉問題。
解決方法是利用已有的舊版本:用舊版 A 建置新版 B,再用新版 B 建置新版 A,形成「建置階梯(Build Ladder)」。

Figure 13.8: Circular dependency build ladder
常見陷阱: 循環依賴不被任何建置系統原生支援,需要 hack 工具鏈。若每個元件自動觸發其依賴的建置,兩個元件會因為循環而永遠建置下去。始終嘗試消除循環依賴,建置階梯只是暫時的權宜之計。
管理二進位檔(Managing Binaries)#
元件之間應該有二進位層級的依賴,而非原始碼層級。管理二進位檔是元件化建置的關鍵。
Artifact Repository 的運作原則#
- 不應包含無法重現的東西:應能刪除整個 repository 而不擔心失去有價值的東西(前提是版本控制包含重建任何二進位檔所需的一切)
- 不要將二進位檔簽入版本控制:因為它們很大且可重建
- 保留通過所有測試的候選發佈版本和已發佈的版本
- 永遠保留每個二進位檔的雜湊值(hash):用於稽核和追溯,能從 MD5 查出建立該二進位檔的原始碼修訂版本
最簡單的 artifact repository 是磁碟上的目錄結構:每個管線一個目錄,其內每個建置編號一個子目錄。加上索引檔案記錄每次建置的狀態,追蹤候選版本通過管線的進度。
部署管線與 Artifact Repository 的互動#
管線各階段的角色:
| 管線階段 | 角色 |
|---|---|
| 編譯階段 | 產生二進位檔,存入 repository |
| 單元/驗收測試階段 | 取出二進位檔執行測試,將報告存回 repository |
| 使用者驗收測試階段 | 取出二進位檔部署到 UAT 環境 |
| 發佈階段 | 取出二進位檔發佈到生產環境 |
sequenceDiagram
participant 編譯階段
participant Repository as Artifact Repository
participant 測試階段
participant UAT
participant 發佈階段
編譯階段->>Repository: 存入二進位產物
測試階段->>Repository: 取出二進位產物
測試階段->>Repository: 存入測試報告
UAT->>Repository: 取出產物部署
發佈階段->>Repository: 取出產物發佈到生產後續階段依賴索引檔中的狀態,確保只有通過驗收測試的二進位檔才可進入手動測試和後續階段。
使用 Maven 管理依賴#
Maven 是 Java 專案的可擴展建置管理工具,提供精密的依賴管理機制。所有 Maven 領域物件(專案、依賴、外掛)由座標唯一識別:**groupId、artifactId、version(GAV)**加上 packaging。
核心機制#
- Maven 社群維護的映射儲存庫包含幾乎所有開源函式庫及其中繼資料(含傳遞性依賴)
- 在
pom.xml中宣告依賴即可自動下載 - 依賴 scope:
test(僅測試時)、runtime(僅執行時)、provided(編譯時需要但執行時由環境提供)、compile(預設,編譯和執行時都需要) - 版本範圍:如
[1.0,2.0)表示 1.x 系列任何版本 - SNAPSHOT 機制:版本後綴
-SNAPSHOT用於開發中版本,但應謹慎使用以免影響建置可重現性
實用建議: 讓 CI 伺服器使用建置標籤作為版本號的一部分來產生正式版本的依賴,存入組織的中央 artifact repository。使用 Maven 的版本量詞指定可接受的版本範圍。使用
mvn dependency:analyze可以發現未宣告的依賴和未使用的已宣告依賴。
Maven 依賴重構#
- Parent POM:使用
<dependencyManagement>在父專案中集中定義依賴版本,子專案引用時無需指定版本 - POM 依賴:將 packaging 改為
pom,其他專案可宣告對此 POM 的依賴,重用相同的依賴集合
總結#
本章討論了在持續變更中保持應用程式可發佈的技術。核心策略是:
- 增量式變更:每個變更都是小的、可發佈的步驟,提交到主線
- 元件化:將應用程式劃分為鬆耦合、良好封裝的協作元件
在應用程式足夠大之前,不需要為每個元件建立獨立管線——最簡單的做法是使用單一管線建置整個應用程式。一個 20 人以下的全職團隊工作兩年內,通常不需要多條建置管線(但仍應將應用程式分為元件)。
超過此規模後,元件化、基於依賴的建置管線和有效的 artifact 管理是高效交付和快速回饋的關鍵。此方法建立在元件化設計的既有優勢之上,避免使用複雜的分支策略(通常會導致嚴重的整合問題),但前提是應用程式設計良好、適合元件化建置。
核心觀點: 確保有效運用技術的工具鏈來撰寫可以被建置為一組獨立元件的程式碼——這在程式碼庫成長到足夠大時至關重要。不要等到應用程式變成難以元件化的龐然大物時才開始,那時重構成本將會非常昂貴。