第 2 / 6 部 待錄

反轉依賴與六邊形架構

《整潔架構實戰》Podcast 準備稿:反轉依賴與六邊形架構

書名: 整潔架構實戰(Get Your Hands Dirty on Clean Architecture) 作者: Tom Hombergs 系列: 設計品味系列 公開標題建議: 設計品味系列|整潔架構實戰.第二集:把依賴箭頭轉個方向,世界就不一樣了 (2/6) 涵蓋範圍: 第 3 章「反轉依賴」、第 4 章「組織程式碼」

背景速覽

上一集我們把分層架構的病講透了——它讓你以資料庫為中心、容易抄捷徑、難測、隱藏使用案例。這一集開藥。藥的核心其實只有一個動作:把依賴的箭頭轉個方向。本書最關鍵的兩集大概就是這一集和下一集,因為這兩章把「為什麼」收斂成「怎麼做」。

第 3 章從兩個 SOLID 原則(SRP 和 DIP)出發,推導出整潔架構與六邊形架構,正式登場「依賴規則」和「埠與轉接器」。第 4 章則回答一個很實際的問題:架構畫得很漂亮,但翻開程式碼,套件結構能不能讓你「一眼看出這是六邊形架構」?這兩章合起來,就是這本書的骨架。

一句話重點

只要用依賴反轉原則把「領域依賴持久化」這個箭頭轉成「持久化依賴領域」,領域程式碼就從外部技術的壓迫中解放出來——這就是整潔架構與六邊形架構共同的唯一核心。

值得討論的重點

重點一:SRP 你大概一直誤解了——它不是「只做一件事」

幾乎每個工程師都會背單一職責原則(SRP),而且幾乎每個人都背錯。常見的解讀是「一個元件應該只做一件事,並把它做好」。作者直接說:這不是 SRP 的本意。

SRP 真正的定義是:一個元件應該只有一個改變的理由(only one reason to change)。作者甚至說,這原則乾脆改名叫「單一改變理由原則」算了。差別在哪?「只做一件事」是看功能;「只有一個改變理由」是看它「會因為什麼而被迫修改」。

為什麼這個重新理解這麼關鍵?因為「改變理由」會透過依賴關係傳染。一個沒有任何依賴的元件 E,唯一的改變理由是「需求要它變」。但一個依賴了一大串其他元件的元件 A,它的依賴每一個都是一個潛在的改變理由——連遞移依賴(間接依賴)都算。很多程式庫越來越難改,根因就是元件不斷累積改變理由,累積到改一個就弄壞另一個。作者還講了一個真實故事:客戶寧可用一個對使用者很彆扭的方式做功能,也不肯改某個核心元件,因為「過去前一個團隊一改它就弄壞別的東西」——這就是爛架構逼客戶多付錢的活案例。

重點二:依賴反轉原則——你可以反轉任何一個依賴的方向

SRP 告訴我們「依賴 = 改變理由」,所以我們最不希望最重要的領域程式碼,去依賴一堆會亂動的東西。但在分層架構裡,領域層偏偏依賴持久化層——持久化一改,領域就被迫跟著改。怎麼辦?

答案是依賴反轉原則(DIP),而且 DIP 名副其實:你可以反轉程式庫中任何一個依賴的方向。具體三步驟:

  1. 先把 entity 上移到領域層(它們本來就代表領域物件)。
  2. 這時持久化層的 repository 反過來依賴領域層的 entity,產生了循環依賴。
  3. 套用 DIP:在領域層定義一個 repository 介面,讓持久化層的真實 repository 去「實作」它。

這樣一來,依賴方向就反轉成「持久化層依賴領域層」,領域邏輯徹底從持久化的壓迫中解放。有一個重要前提作者特別提醒:只有當你同時掌控依賴的兩端時才能反轉。如果依賴的是第三方函式庫,你不掌控它的程式碼,就反轉不了。

重點三:整潔架構 vs 六邊形架構——其實是同一件事的兩個說法

作者很乾脆地把兩個常被混用的詞釐清:

  • 整潔架構(Clean Architecture):Uncle Bob 提的,畫成同心圓,核心規則叫「依賴規則」——所有跨層依賴都必須指向內側。最內是 entity,外一圈是 use case(就是更細的服務,各自單一職責,解決過寬服務問題),再外圈是提供持久化、UI 的 adapter。代價是:因為領域和外層完全解耦,你得在每一層各維護一份模型、在層間對映(第 9 章的主題)。
  • 六邊形架構(Hexagonal Architecture):Alistair Cockburn 更早提的,把應用核心畫成六邊形,又叫「埠與轉接器(Ports and Adapters)」。六邊形沒有任何朝外依賴,所以 Uncle Bob 的依賴規則自動成立。外面圍著各種 adapter,分兩類:驅動 adapter(在左邊,呼叫核心,比如 Web)和被驅動 adapter(在右邊,被核心呼叫,比如資料庫)。核心對外提供「埠(port)」——驅動側的 port 是核心實作、adapter 呼叫;被驅動側的 port 是 adapter 實作、核心呼叫。

作者點破:六邊形架構的本質,就是依賴反轉原則在「架構層級」的應用——核心定義並擁有對外的介面(port),adapter 去配合。所以整潔架構和六邊形架構,是同一個直覺在不同抽象層級的表達。本書接下來就用六邊形這套語言。

重點四:用程式碼「喊出」架構——富表達力的套件結構

第 4 章解決一個很實際的痛:你架構圖畫得再漂亮,如果翻開程式碼套件結構看不出來,那就有「架構/程式碼落差」——架構只是 PPT 上的抽象概念,時間一久程式碼就漂走了。

作者先否決兩個常見做法:

  • 依層組織(web/domain/persistence 各一包):看不出有哪些使用案例、看不出目標架構,功能之間還沒有邊界。
  • 依功能組織(把帳戶相關的全塞進 account 包):能讓人「看名字就知道功能」(Uncle Bob 說的「尖叫的架構」),但反而更看不出 adapter 和 port,而且保護不了領域不去依賴持久化。

作者的解法是一套「富表達力的套件結構」,直接對映六邊形的元素:

  • 最高層是 adapterapplication 兩包。
  • adapter 包:放 web、persistence 各自的子包,裡面的類別都可以是 package-private(外界只透過 port 介面呼叫),所以不會意外被依賴。
  • application 包:放 domain(實體 + 領域服務)和 port(埠介面)。port 為什麼放在 application 裡面 而不是並列?因為這正表達「應用擁有這些 port」——port 是應用對外溝通的方式,是依賴反轉的工具。

最後作者帶出全章的收尾:依賴注入(DI)。這套結構有個鐵律——應用層不能依賴 adapter。輸入 adapter 好辦(控制流和依賴同方向,直接呼叫服務);輸出 adapter 就得靠 DIP(在應用層定義 port,adapter 實作)。那誰來把實作塞給應用?一個「對架構中立、依賴所有層」的 DI 元件——它建出 controller、service、persistence adapter,再把實作以 port 介面的身分注入進去,讓上層永遠只認識介面、不認識實作。

注意事項

⚠️ 整潔/六邊形架構不是免費的。作者自己在第 3 章就用 [!IMPORTANT] 標出來:因為領域和外層完全解耦,你必須在每一層維護一份模型,並在層間對映。一個簡單的 CRUD,可能因此從「一個 class 走天下」變成「Web 模型 → 領域模型 → 持久化模型」三份加兩組對映器。對複雜領域這很值得;對純 CRUD 這就是樣板地獄。這是本書反覆出現的張力,第 9、11、15 章都會回來談。

⚠️ 「把依賴反轉就解放領域」聽起來很乾淨,但 DIP 有個常被忽略的前提——你得同時掌控兩端。對第三方函式庫、對你無法修改的遺留系統、對某些框架強加的結構,你根本反轉不了,只能用 adapter 包一層去隔離。所以實務上六邊形架構的價值,與其說是「反轉了所有依賴」,不如說是「把不可控的外部隔離在 adapter 裡,讓核心保持可控」。

⚠️ 「尖叫的架構」(用套件名喊出功能)和「富表達力的套件結構」(用套件名喊出六邊形元素)其實有點互相拉扯——前者強調「依功能分」,後者強調「依架構角色分」。本書選了後者,但對小團隊、小應用,依功能分往往更直覺好找。沒有絕對答案,重點是團隊一致、能縮小「架構/程式碼落差」就好。別為了套書上的結構,把一個三人小專案搞出十層套件。

專家補充

💡 六邊形為什麼是「六」邊?作者講了個冷知識:形狀本身沒意義,畫成八邊形也行。傳說 Cockburn 用六邊形取代矩形,是要表達「應用可以有超過四個邊」來接不同的 adapter——打破「Web 在上、DB 在下」那種上下層的視覺暗示,改成「核心在中間,四面八方都是平等的 adapter」。這個視覺隱喻很重要:它幫你戒掉「資料庫在最底層」的下意識。

💡 DIP 在架構層級的應用,有個很實際的副產品叫「可測試性」。因為 port 是介面、實作靠注入,你做測試時可以注入一個 in-memory 的假 adapter,根本不用啟動真資料庫。這也是為什麼第 8 章能講出一套那麼乾淨的測試策略——可測試性不是架構的附贈品,它是依賴反轉的直接結果。port 越小、越專注,mock 越輕鬆。

💡 「應用擁有 port」這個說法,跟微服務時代的「API First」「契約優先」是同一個精神的不同尺度。在這本書裡,是「應用核心定義它對外暴露什麼介面,adapter 來配合」;在微服務裡,是「服務定義它的 API 契約,呼叫端來配合」。你在單體裡把 port 設計好的這套肌肉,日後拆服務時定義 API 契約會直接受益。

💡 SRP 那個「改變理由」的重新詮釋,其實是這本書(乃至 Uncle Bob 整套思想)的隱形主線。後面幾乎每一個設計決定——為什麼每個 use case 一個 service、為什麼每個操作一個 controller、為什麼 port 要切細——回到根上都是同一句:減少每個元件的「改變理由」數量。你可以把這集的 SRP 當成整本書的解碼器。

討論問題

🎙️ 你以前是怎麼理解 SRP 的?是「只做一件事」還是「只有一個改變理由」?換成「改變理由」這個角度後,你手上有沒有哪個 class 立刻浮現——它其實背了三四個完全不同的改變理由?

🎙️ 你的專案如果現在被人 clone 下來、只看套件結構,看得出用了什麼架構嗎?還是只能看到 controller / service / repository 三包,完全猜不到設計意圖?

🎙️ 「整潔架構要在每層維護一份模型、做層間對映」——這個代價,你覺得在你現在的專案值得嗎?哪些模組值得、哪些根本是 CRUD 不該硬上?

🎙️ 你有沒有遇過「想反轉某個依賴卻反轉不了」的情況(因為它是第三方或你不掌控的)?那時你是怎麼用 adapter 把它隔離掉的?

更大範圍關聯

  • 接 Uncle Bob《Clean Architecture》:本集的「整潔架構同心圓 + 依賴規則」正是出自那本。Tom 在這裡把它和 Cockburn 的六邊形並列、點破兩者同源,這個串連本身就很有教學價值。

  • 接 Alistair Cockburn 的「埠與轉接器」原始文章:六邊形架構的本尊。作者特別強調 Cockburn 的本意是「封裝一個應用」而非「封裝一個限界上下文」——這個區分到第 13 章會變成關鍵伏筆。

  • 接 SOLID 原則(尤其 DIP):本集是 SOLID 的「架構級實戰」。如果聽眾讀過 SOLID 卻覺得「知道但用不上」,這集正好示範 SRP 和 DIP 在架構層級威力有多大。第 7 章還會補上 ISP(介面隔離)。

  • 接 DDD:「把領域放中心、依賴都指向領域」是 DDD 能落地的前提。作者第 15 章會明說:六邊形這種以領域為中心的架構,是 DDD 的「促成者」——沒有它,DDD 的設計總會被技術因素牽著走。

錄製建議

目標約 20 分鐘,單人主講。建議節奏:

  • 0–2 分:開場接上集。「上集講病,這集開藥,而藥只有一個動作——把依賴箭頭轉個方向」。
  • 2–7 分:第 3 章前半。SRP 的「你一直誤解它」切入最抓人,用「只做一件事 vs 只有一個改變理由」的對比,再帶那個「客戶不敢改核心元件」的真實故事。
  • 7–13 分:第 3 章後半,本集核心。DIP 三步驟反轉「領域與持久化」依賴,講慢一點;接著整潔架構(同心圓 + 依賴規則)和六邊形架構(埠與轉接器、驅動/被驅動)並列,點破兩者同源。
  • 13–17 分:第 4 章。從「架構/程式碼落差」這個痛切入,快速否決依層/依功能兩種組織法,帶出「富表達力套件結構 + port 放在 application 裡 = 應用擁有 port」,最後落到 DI 的角色。
  • 17–20 分:辯證 + 收尾。誠實講「這套架構不免費,每層一份模型 + 對映是真實成本」,預告下一集真正動手寫第一個 use case(SendMoneyService),看這套結構跑起來。

口氣提醒:這集概念密度最高,務必多用比喻(依賴箭頭、樂高、六邊形的四面八方),少念定義。SRP 那段可以停下來讓聽眾「對號入座」自己專案裡的 God Class。