我們已實作了一些使用案例、Web adapter 與持久化 adapter,現在要把它們組裝成可運作的應用。如第 4 章所述,我們依賴依賴注入機制,在啟動時實例化類別並把它們接線在一起。本章討論用純 Java、以及 Spring 與 Spring Boot 框架來做這件事的幾種做法。
為什麼要在意組裝?#
為什麼不在需要時、需要的地方直接實例化使用案例與 adapter?因為我們要讓程式碼依賴指向正確方向:所有依賴都應指向內側、朝向領域程式碼,這樣外層變動時領域程式碼才不必跟著改。
- 若某使用案例需要呼叫持久化 adapter 卻自行實例化它,就製造了一個方向錯誤的程式碼依賴。
- 這正是我們建立輸出 port 介面的原因:使用案例只認識介面,執行期才被提供該介面的實作。
這種程式風格還有個好副作用:程式碼更易測試。若能把類別所需的所有物件都透過建構子傳入,就能選擇傳入 mock 而非真實物件,輕鬆為類別建立隔離的單元測試。
那麼,誰負責建立物件實例?又如何在不違反依賴規則的前提下做到?答案是:必須有一個對架構中立、且依賴所有類別的設定元件來實例化它們。在第 3 章的整潔架構中,這個設定元件位於最外圈,依依賴規則它可存取所有內層。它必須:
- 建立 Web adapter 實例。
- 確保 HTTP 請求確實被路由到 Web adapter。
- 建立使用案例實例,並提供給 Web adapter。
- 建立持久化 adapter 實例,並提供給使用案例。
- 確保持久化 adapter 能真正存取資料庫。

Figure 10.1: 一個對架構中立的設定元件可存取所有類別以實例化它們
此外,設定元件還應能存取設定參數來源(如設定檔或命令列參數),並在組裝期把這些參數傳給各元件,以控制「存取哪個資料庫」「用哪台伺服器寄信」等行為。
這麼多職責(也就是這麼多改變理由),不是違反單一職責原則了嗎?是的。但若想讓應用其餘部分保持乾淨,就需要一個外部元件來負責接線,而這個元件必須認識所有零件才能把它們組裝成可運作的應用。
用純程式碼組裝#
若不靠依賴注入框架,可用純程式碼建立負責組裝的設定元件:
- Java 應用從
main方法啟動。在其中實例化所有需要的類別(從 Web controller 到持久化 adapter),並把它們接線在一起。 - 最後呼叫一個
startProcessingWebRequests()之類的方法,透過 HTTP 暴露 Web controller,應用便可開始處理請求。(此方法只是「透過 HTTP 暴露 Web adapter 所需引導邏輯」的佔位符——真實應用中由框架代勞,我們不想自己實作。)
純程式碼是最基本的組裝方式,但有缺點:
- 上述程式碼只對應「僅有單一 Web controller、使用案例與持久化 adapter」的應用。想像要引導一個完整的企業級應用,得寫多少這種程式碼!
- 由於我們從各類別的套件外部親手實例化它們,這些類別全得是 public。如此一來,Java 編譯器便無法阻止使用案例直接存取(public 的)持久化 adapter。若能用 package-private 可見性避免這種不想要的依賴就好了。
所幸有依賴注入框架能代勞這些雜務,同時仍維持 package-private 依賴。Spring 是目前 Java 世界最受歡迎的框架,它還提供 Web 與資料庫支援等功能,讓我們不必親自實作那個神祕的 startProcessingWebRequests()。
透過 Spring 的 classpath 掃描組裝#
用 Spring 組裝應用時,結果稱為應用情境(application context),其中包含構成應用的所有物件(Java 術語稱 bean)。Spring 提供多種組裝方式,先談最受歡迎也最方便的:classpath 掃描。
- Spring 走訪 classpath 某切片中所有可用的類別,尋找標註了
@Component的類別,並為每個建立物件。這些類別應有一個「以所有必要欄位為參數」的建構子(如第 7 章的AccountPersistenceAdapter)。 - 本例甚至不必自己寫建構子,而用 Lombok 的
@RequiredArgsConstructor標註自動產生「以所有 final 欄位為參數」的建構子。 - Spring 會找到此建構子,為各參數型別尋找標註
@Component的類別並以類似方式實例化、加入情境;待所有必要物件就緒,再呼叫AccountPersistenceAdapter的建構子,把結果物件加入情境。
我們也能自訂供 Spring 辨識的「刻板標註(stereotype annotation)」,例如建立 @PersistenceAdapter:
- 它以
@Component做後設標註(meta-annotated),讓 Spring 在 classpath 掃描時辨識它。 - 改用
@PersistenceAdapter取代@Component標記持久化 adapter,能讓讀程式碼的人更清楚看出架構。
classpath 掃描的缺點:
- 侵入性:要求我們在類別上加框架專屬標註。整潔架構的硬派人士會說這是禁忌,因為它把程式碼綁到特定框架。作者則認為一般應用開發中,類別上一個標註不算大事、必要時也易於重構;但若是為其他開發者打造函式庫或框架,這就可能是禁區——我們不想讓使用者背上對 Spring 的依賴。
- 可能發生「魔法」:指那種難以解釋、若非 Spring 專家可能要花好幾天才搞懂的壞魔法。它之所以發生,是因為 classpath 掃描是組裝應用的「鈍器」——我們只是把 Spring 指向應用的父套件、叫它去找標
@Component的類別。你能把應用中每個類別都背得滾瓜爛熟嗎?多半不能。難保沒有我們其實不想放進情境的類別,甚至某個會用邪惡方式操弄情境、引發難追蹤錯誤的類別。
透過 Spring 的 Java Config 組裝#
若說 classpath 掃描是組裝的「棍棒」,Spring 的 Java Config 就是「手術刀」。它類似本章稍早的純程式碼做法,但更乾淨、且有框架支撐,不必全部手刻。
- 做法是建立設定類別,每個負責建構一組要加入情境的 bean。例如建立一個負責實例化所有持久化 adapter 的設定類別。
@Configuration標註標記此類別為「供 Spring classpath 掃描辨識的設定類別」。因此這裡仍用 classpath 掃描,但只辨識設定類別、而非每個 bean,降低了壞魔法發生的機率。- bean 在設定類別的
@Bean工廠方法中建立。上例把一個持久化 adapter 加入情境,其建構子需要兩個 repository 與一個 mapper,Spring 會自動把這些物件作為輸入提供給工廠方法。
那 Spring 從哪取得 repository 物件?
- 若它們在另一個設定類別的工廠方法中手動建立,Spring 會自動把它們作為參數提供。
- 但本例中它們由 Spring 自己建立,由
@EnableJpaRepositories標註觸發:Spring Boot 一旦找到此標註,就會自動為我們定義的所有 Spring Data repository 介面提供實作。
熟悉 Spring Boot 的人或許知道
@EnableJpaRepositories可加在主應用類別上。確實可行,但這樣每次啟動應用都會啟用 JPA repository,即使在不需持久化的測試中也是如此。把這種「功能標註」移到獨立的設定「模組」中,我們就變得更有彈性——可以只啟動應用的一部分,而非總是啟動整個應用。
PersistenceAdapterConfiguration 類別因此成為一個範圍緊湊的持久化模組,實例化持久化層所需的所有物件。它會被 classpath 掃描自動辨識,而我們仍完全掌控「究竟哪些 bean 被加入情境」。同理可為 Web adapter、或應用層中的某些模組建立設定類別。好處包括:
- 能建立「含某些模組、但 mock 掉其他模組 bean」的情境,在測試中極具彈性。
- 幾乎不需重構,就能把每個模組的程式碼推進各自的程式庫、套件或 JAR 檔。
- 不必像 classpath 掃描那樣到處撒
@Component標註,因此應用層可保持乾淨、不依賴 Spring(或任何框架)。
這個方案有個陷阱:若設定類別與它所建立 bean 的類別(本例的持久化 adapter)不在同一套件,那些類別就必須是 public。要限制可見性,可把套件當作模組邊界、在每個套件內建立專屬設定類別——但這樣就不能使用子套件,原因詳見第 12 章。
這如何幫助我打造可維護的軟體?#
Spring 與 Spring Boot(及類似框架)提供許多讓生活更輕鬆的功能,其中之一就是「把我們提供的零件(類別)組裝成應用」。
- classpath 掃描非常方便:只需把 Spring 指向一個套件,它就能用找到的類別組裝出應用,讓開發迅速、不必把應用當作整體去思考。但程式庫一旦變大,這很快導致透明度不足——我們不知道究竟哪些 bean 被載入情境,也難以為了測試而獨立啟動情境的一部分。
- 透過建立專屬設定元件負責組裝,我們能把這份責任(也就是「改變理由」——還記得 SOLID 的「S」嗎?)從應用程式碼中解放出來,換得高度內聚、可彼此隔離啟動、且能輕鬆在程式庫中移動的模組。一如往常,代價是花些額外時間維護這個設定元件。
本章與前幾章談了許多「正確做法」。但有時「正確做法」並不可行。下一章來談捷徑——我們為它付出的代價,以及何時值得一走。