《整潔架構實戰》Podcast 準備稿:邊界對映與組裝應用
書名: 整潔架構實戰(Get Your Hands Dirty on Clean Architecture) 作者: Tom Hombergs 系列: 設計品味系列 公開標題建議: 設計品味系列|整潔架構實戰.第五集:層間對映到底要不要做?以及誰來把零件組裝起來 (5/6) 涵蓋範圍: 第 9 章「邊界之間的映射」、第 10 章「組裝應用程式」
背景速覽
走到這裡,六邊形的零件都齊了——核心、Web adapter、persistence adapter、測試。這一集處理兩個「把零件串起來」時繞不開的現實問題:第一,各層都有自己的模型,那層與層之間到底要不要做對映(mapping)?這是團隊裡最容易爆發宗教戰爭的議題之一。第二,這麼多用 port 解耦的零件,最後是誰、又怎麼把它們實例化、接線在一起,組裝成一個能跑的應用?
這集的精神跟前幾集略有不同——前面多在講「該怎麼做」,這集作者反覆強調的是「沒有單一正解、要看情況、可以混用、可以演進」。對映策略他一口氣給了四種、各有優缺,然後堅決反對「全程式庫只准用一種」的潔癖。組裝則從純 Java 講到 Spring 的兩種方式,核心是「把組裝這個職責從應用程式碼裡隔離出去」。
一句話重點
層間對映沒有銀彈——四種策略各有取捨,該為「每個使用案例」個別選擇、甚至隨時間演進;而把這些解耦零件組裝成應用,需要一個「對架構中立、依賴所有層」的設定元件,獨自扛下這個改變理由。
值得討論的重點
重點一:那場永恆的戰爭——要不要做層間對映?
作者開場就還原了團隊裡的經典對立:
- 挺對映派:「不對映就得在兩層共用同一模型,這會讓層緊密耦合!」
- 反對映派:「但層間對映會生出一堆樣板程式碼,對只做 CRUD、各層本來就共用模型的 use case 來說根本殺雞用牛刀!」
作者的態度是——兩邊都對。所以他不選邊,而是給你四種策略和各自的適用場景,讓你為每個 use case 個別決定。這個「拒絕一刀切」的立場,本身就是這章最重要的訊息。
重點二:四種對映策略的光譜(從最省事到最解耦)
作者把四種策略排成一個光譜,從「最省事但最耦合」到「最解耦但最費工」:
-
不對映(No Mapping):所有層共用同一個領域模型,零對映。爽,但後果是領域模型
Account上得同時掛 JSON 標註(給 Web)和 ORM 標註(給持久化),這違反 SRP——Account會因為 Web、持久化的理由被迫改變,領域模型被汙染碎片化。但作者明說它在「所有層需要的資訊結構完全相同」時完全有效,尤其是簡單 CRUD。判準:一旦應用/領域層開始處理 Web 或持久化的議題(標註或許除外),就該換策略。 -
雙向對映(Two-Way):每層有自己的模型,adapter 負責把自己的模型 → 領域模型 → 自己的模型雙向對映。優點是領域模型乾淨、各層能獨立修改自己的模型、責任明確(外層對映、內層只認自己的模型)。缺點是大量樣板,而且除錯對映邏輯很痛苦(尤其框架把細節藏在反射後面)。
-
完整對映(Full):每個「操作」都有自己專屬的輸入輸出模型(像
SendMoneyCommand),不再用領域模型跨層溝通。好處是應用層介面極度明確、逐操作的驗證清晰。作者不主張全域用它,而是建議只用在「Web 與應用」之間(標示修改狀態的 use case),且通常只用在輸入側、輸出直接回領域物件——這恰恰示範了「對映策略可以、也應該混用」。 -
單向對映(One-Way):所有層的模型都實作同一個「狀態」介面(用 getter 封裝狀態)。領域物件已實作這個介面,傳給外層不需對映;外層收到他層物件時,自己對映成能用的東西。每層只對映「收到的方向」,故名單向。它跟 DDD 的「工廠(factory)」概念契合(從某狀態重建領域物件)。在「各層模型相似」、尤其唯讀操作時最強。缺點是對映分散各層,概念上較難。
重點三:何時用哪種?「看情況」——但要把理由寫下來
到了「該選哪種」這個百萬問題,作者的答案一如往常:「看情況。」但他給了比這句話更有用的東西——反對「為整個程式庫定一條鐵律」。
他講得很實在:在同一個程式庫裡混用不同模式,感覺很不整潔,違反直覺。但如果只為了滿足「整潔感」,明知某模式不是這個 use case 的最佳解還硬用,那才是不負責任。而且軟體會演進——昨天最適合的策略,今天未必。他的建議是先用簡單策略(讓程式碼快速演進),日後再轉向更複雜、更解耦的策略。
更關鍵的是:團隊要約定一組準則,回答「什麼情況首選哪種」以及「為什麼」(這樣日後能評估理由是否還成立)。他給了範例準則,比如:
- 修改型 use case,Web 與應用之間首選「完整」對映(解耦各 use case、逐案例驗證清晰);
- 修改型 use case,應用與持久化之間首選「不對映」(無對映開銷快速演進),一旦應用層開始碰持久化議題,就轉「雙向」;
- 查詢操作,兩側都首選「不對映」快速演進,碰到議題再轉「雙向」。
而這些準則要有用,必須活在開發者腦中、是團隊持續討論修訂的共識,不是寫在 wiki 上積灰的死規則。
重點四:誰來組裝應用?一個「對架構中立、依賴所有層」的設定元件
第 10 章回答「這麼多解耦零件,誰把它們接起來」。為什麼要在意組裝?因為要維持「所有依賴都指向內側、朝向領域」。如果某個 use case 自己 new 出一個 persistence adapter,就製造了一個方向錯誤的依賴。所以需要一個對架構中立、且依賴所有類別的設定元件——它在整潔架構的最外圈,負責建出 Web adapter、use case、persistence adapter,並把實作以 port 介面的身分注入進去。
作者誠實面對一個矛盾:這個設定元件扛了一大堆職責(這麼多改變理由),不就違反 SRP 了嗎?是的。但若想讓應用其餘部分保持乾淨,就得有一個外部元件犧牲自己、認識所有零件來做接線。他講了三種做法:
- 純 Java(main 方法手刻):最基本,但缺點明顯——企業級應用要手寫一堆接線程式碼;而且因為從套件外部親手實例化,所有類別都得是 public,編譯器就擋不住 use case 直接存取 persistence adapter。
- Spring 的 classpath 掃描:最方便(撒
@Component、Spring 自動找來組裝),但兩個缺點——侵入性(類別被綁上框架標註)和「魔法」(你說不清到底哪些 bean 進了 context,可能混進不該進的、甚至會搞鬼的類別)。 - Spring 的 Java Config:作者偏好的「手術刀」。用
@Configuration設定類別、@Bean工廠方法明確建立 bean,把每個模組(如 persistence)做成範圍緊湊的設定模組。好處是完全掌控哪些 bean 進 context、能只啟動應用的一部分做測試、應用層可保持乾淨不依賴 Spring、幾乎不重構就能把模組推進各自的 JAR。代價是要多花時間維護這個設定元件。
注意事項
⚠️ 四種對映策略 + 「為每個 use case 個別選 + 可混用」聽起來很自由,但對團隊紀律是巨大考驗。一個程式庫裡同時存在不對映、雙向、完整三種策略,新人讀起來會很困惑「為什麼這裡這樣、那裡那樣」。作者的解藥是「把準則和理由寫下來、活在團隊腦中」——但這套東西一旦沒人維護、人員一輪替,就會退化成「看誰寫的、心情好壞」的混亂。自由的代價是更高的溝通與文件成本,這點要對聽眾講白。
⚠️ 「先用簡單策略、日後再演進到複雜策略」這個建議很對,但有個現實陷阱——演進的時機很難抓、而且常常永遠不會發生。一個用「不對映」起步的 use case,等它長複雜了再來抽對映層,那時程式碼已經到處共用領域模型、牽一髮動全身,重構成本遠高於當初。這跟第一集「之後再整理永遠不會來」是同一個風險。務實的做法是:在團隊準則裡明確定義「什麼信號出現就該升級策略」,並真的有人盯著這些信號。
⚠️ 那個設定元件「違反 SRP」這件事,作者是當成「必要之惡」接受的。但要小心它變成一個失控的上帝元件——一個塞滿幾百個 @Bean 方法、誰都看不懂的巨型 config。Java Config 的「按模組切設定類別」就是在控制這個風險。如果你的組裝層變成一坨,那不是 SRP 救不了,而是你沒按模組去切。
⚠️ classpath 掃描的「魔法」批評很中肯,但 Java Config 的「手術刀」也不是免費的——它更囉嗦、更多樣板,對一個十幾個 bean 的小應用,掃描的便利往往勝過「完全掌控」帶來的好處。作者自己也說掃描「非常方便、讓開發迅速」。別因為書上偏好 Java Config,就在小專案裡放棄掃描的生產力。
專家補充
💡 這四種對映策略,其實可以對應到業界不同社群的習慣。「不對映」是大多數 Rails / Django / 簡單 Spring Boot CRUD 的預設(一個 model 走天下)。「雙向對映」是企業 Java / DDD 嚴格派的標配(DTO → Domain → Entity 三層分明)。「完整對映」非常接近 CQRS 裡「每個 command/query 一個專屬模型」。「單向對映 + 狀態介面」則和函數式、以及 DDD 工廠模式同源。所以這章其實是一張「對映策略的世界地圖」,聽眾可以拿來定位自己團隊現在站在哪。
💡 對映樣板程式碼的痛,催生了一整個工具生態——Java 的 MapStruct(編譯期生成對映、無反射)、早年的 ModelMapper/Dozer(反射、慢且難除錯,正是作者吐槽的那種)。作者說「除錯對映邏輯很痛苦,尤其框架把細節藏在泛型和反射後面」——這句話幾乎就是在點名 Dozer 那一代。現代的解法是 MapStruct 這種編譯期生成的,能大幅降低「雙向/完整對映」的痛。這集可以順帶推坑:如果你還在手刻或用反射對映器,MapStruct 值得一看。
💡 「設定元件對架構中立、依賴所有層、位於最外圈」這個角色,在 Uncle Bob 的同心圓裡有個正式名字——它屬於最外圈的「Main 元件」。Uncle Bob 在《Clean Architecture》專門有一章講 Main,說它是「最髒的元件」——它知道一切、依賴一切,好讓內圈保持乾淨。本書的設定元件就是 Main 的 Spring 化身。可以把這個串連講給聽眾,讓「為什麼這個元件可以違反 SRP」變得理直氣壯——因為它的職責就是「當那個犧牲自己、扛下所有接線的髒角色」。
💡 「準則要活在開發者腦中、是持續討論的共識」這句,其實點到了架構治理(architecture governance)的核心難題——架構決策不是寫一次文件就完事,而是要持續被傳遞、被質疑、被更新。這也是為什麼第 11 集會講到 ADR(架構決策記錄):把「為什麼選這個」記下來,才能在日後理由失效時被重新評估。對映準則 + ADR,是同一個「讓決策可追溯、可演進」的精神。
討論問題
🎙️ 你的團隊現在是哪種對映策略?是「一個 model 走天下」的不對映,還是「DTO/Domain/Entity 三層分明」的雙向對映?這是團隊「決定」的,還是「不知不覺就變這樣」的?
🎙️ 作者反對「整個程式庫只准用一種對映策略」。你能接受在同一個 codebase 裡,這個 use case 不對映、那個 use case 完整對映嗎?還是你會覺得「不整潔」而渾身不對勁?這份不對勁,是潔癖還是有道理?
🎙️ 你有沒有一個用「不對映」起步、後來長複雜了、卻一直沒勇氣抽對映層的 use case?現在要抽,成本是不是已經高到不敢動了?
🎙️ 你的應用是怎麼組裝的——main 手刻、classpath 掃描、還是 Java Config?你說得清楚「到底哪些 bean 進了 context」嗎,還是有一堆你也不確定為什麼會在那的「魔法」?
更大範圍關聯
-
接 Uncle Bob《Clean Architecture》:第 10 章的「對架構中立的設定元件」就是 Uncle Bob 筆下最外圈的「Main 元件」——那個知道一切、依賴一切、最髒但讓內圈乾淨的角色。本集是 Main 元件的 Spring 落地。
-
接 DDD:單向對映呼應 DDD 的「工廠」模式;「每個 use case 專屬模型」呼應 DDD 對模型邊界的重視。對映策略的選擇,本質是「不同限界上下文/層之間如何翻譯模型」的問題,第 13 集還會在限界上下文層級重演一次。
-
接 CQRS:「完整對映」(每個操作一個專屬模型)幾乎就是 CQRS 在模型層的體現,延續第三集講的 command/query 分離。
-
接依賴注入框架的演進(Spring、Guice、Dagger):第 10 章的三種組裝方式,是「DI 容器要多少魔法、開發者要多少掌控」這個永恆權衡的縮影。classpath 掃描 vs Java Config,就是「約定優於配置」與「顯式優於隱式」兩種哲學的對撞。
錄製建議
目標約 20 分鐘,單人主講。建議節奏:
- 0–2 分:開場。「零件都齊了,這集處理兩個串零件時繞不開的現實問題——層間要不要對映、誰來組裝」。
- 2–5 分:還原「對映 vs 不對映」那場永恆戰爭,挑明作者的態度是「兩邊都對、不選邊、為每個 use case 個別選」。
- 5–11 分:四種對映策略光譜,從不對映 → 雙向 → 完整 → 單向,每種一句優點一句缺點一個適用場景。這段是本集主菜,用「從最省事到最解耦」的軸線串起來,別讓它變成四個孤立條目。
- 11–14 分:「何時用哪種——看情況」+ 反對一刀切 + 把理由寫下來、活在腦中。這是全集的價值觀核心,講透。
- 14–18 分:第 10 章組裝。從「誰來接線、為什麼不能自己 new」切入,帶出「對架構中立的設定元件」(點名它就是 Uncle Bob 的 Main),快掃純 Java / classpath 掃描 / Java Config 三種做法的取捨。
- 18–20 分:辯證 + 收尾。誠實講「自由混用的代價是更高的溝通與文件成本」「演進時機很難抓」,預告最後一集——捷徑、架構邊界強制、限界上下文、以及那個終極問題『到底何時該用這套架構』。
口氣提醒:這集的靈魂是「拒絕一刀切、擁抱看情況」,跟前幾集「這樣做」的口吻不同,要把這個轉變講出來。四種策略容易講成枯燥清單,務必用「你的團隊現在站在哪」的代入感串起來。