我們討論架構時,往往專注於高層次的元件劃分(如 UI, Interactor, Database)。
但當我們真正打開 IDE 看程式碼時,目錄結構往往又是另回事。 Simon Brown 在這章提出了一個尖銳問題:「你的目錄結構(Packaging)是否反映了你的架構設計?」

他以一個「線上書店(訂單查詢)」的使用案例為例,比較了四種主流的程式碼組織策略。

一、四種程式碼組織策略總覽#

打包策略切分依據結構範例優點缺點
逐層打包 (by Layer)技術角色(水平)controllers/, services/, repositories/簡單,好起步無法尖叫;封裝洩漏
依功能打包 (by Feature)領域概念(垂直)orders/, books/, customers/符合尖叫架構若無存取控制,只是視覺分組
Ports and Adapters內外之分(六邊形)domain/(內)+ infrastructure/(外)領域邏輯與框架解耦需嚴格遵守依賴方向(外 → 內)
依元件打包 (by Component)粗粒度元件OrdersComponent/(對外只有 Service 是 public)真正的封裝;強制經過 Service需嚴格控制存取修飾符

Figure 34.1: Package by layer

Figure 34.2: Package by feature

1. 逐層打包的問題#

  • 任何一個領域的變更,幾乎都會跨越所有層;修改檔案散落在各處
  • 從頂層目錄完全看不出這是訂單系統、書店還是任何業務——它只尖叫:「我是 MVC」
  • 規模一大就必須再模組化,逐層切分的方式撐不起真實專案

2. 依功能打包的改善與侷限#

  • 頂層目錄變成 orders/, books/——開始向領域靠近
  • 但若內部類別皆為 public,外層仍能穿越封裝直接存取 Repository,達不到真正的隔離

二、Ports and Adapters:內外之分#

Martin 在前文提過的 Hexagonal Architecture、Onion Architecture 與「邊界—控制器—實體」都屬於同一家族。
它們的共同精神是:將領域邏輯(Inside)與技術細節(Outside)徹底分離。

Figure 34.3: A code base with an inside and an outside

  • Inside(領域): 封裝所有業務概念,命名使用領域通用語言(Ubiquitous Language)——例如 Orders,而非 OrdersRepository
  • Outside(基礎設施): UI、資料庫、第三方整合
  • 依賴鐵律: 外部依賴內部,內部永遠不反向依賴外部

Figure 34.4: View orders use case

三、依元件打包:終極整合#

Simon Brown 在本書提出了第四種——也是他偏好的——組織方式:Package by Component
它結合了 Package by Feature 的領域感,以及 Ports and Adapters 的封裝精神。

1. 核心思想#

將與單個粗粒度元件(如「訂單處理」)有關的所有責任綑綁到單一 package 中:

  • 對外: 只有 OrdersComponent(Service 介面)是 public
  • 對內: OrdersRepository, OrdersController, OrdersImplementation 都是 package-private

這等於在單體(Monolith)內部,預先切出一個「微服務形狀」——未來要拆成獨立服務時,成本極低。

Figure 34.5: Relaxed layered architecture

Figure 34.6: View orders use case

2. 為何勝出?#

  • 單一入口: 所有與訂單有關的程式碼都在 OrdersComponent 內,找得到、改得到
  • 強制封裝: Controller 想跨過 Service 直接存取 Repository?編譯器直接擋下
  • 漸進式遷移: 從單體起步,邊界夠清楚時可無痛升級為微服務

四、魔鬼藏在細節中#

上述四種分法若所有類別都設為 public,在編譯器眼中會完全一模一樣——差別只存在於架構師的想像中。

Figure 34.7: All four architectural approaches are the same

1. 架構的幻覺#

  • 若把嚴格層級圖畫得再漂亮,卻讓每個類別都 public,那只是自我催眠
  • 新進開發者為了趕進度,會毫無阻礙地寫出跨層級、跳過 Service 的依賴
  • 靠「Code Review + 自律」維繫架構,在交付壓力下必然失守

2. 讓編譯器當守門員#

與其仰賴紀律,不如讓語言機制強制執行:

  • Java: 善用 package-private(預設存取修飾符),只把跨 package 介面標為 public
  • C#: 利用 internal 限制組件外可見性
  • 靜態分析工具(NDepend、Structure101、ArchUnit) 可作為補強,但回饋迴圈比編譯器慢

Figure 34.8: Access modifier restrictions

Figure 34.9: Domain and infrastructure code

3. 組織 vs. 封裝(Organization vs. Encapsulation)#

  • 「分組」(放到不同 package)只是組織,不是封裝
  • 真正的封裝來自「讓編譯器拒絕不該發生的依賴」
  • 好架構不是檔案放得多整齊,而是錯誤的依賴寫不出來

不要讓你的架構圖變成紙上談兵。
選擇 「依元件打包」,並用存取修飾符(Access Modifiers) 隱藏實作細節。
讓編譯器成為你的架構守門員——這就是 Simon Brown 留給我們的最後一條建議。