字數總計:0 個 | 閱讀時長:0 分鐘 |閱讀次數: 次
Clean Architecture 第34章:The Missing Chapter
1. 章節主旨
本章由 Simon Brown 撰寫,探討了一個常被忽略但極其重要的話題:即使有良好的架構設計,如果在實作細節上處理不當,仍可能導致整個架構崩壞。作者通過一個線上書店的訂單查詢功能為例,展示了不同的程式碼組織方式及其優缺點。
2. 核心問題:程式碼組織方式的選擇
2.1 Package by Layer(按層級分包)
com.company.myapp/
└── web/
└── OrdersController.java // 處理 HTTP 請求
└── service/
└── OrdersService.java // 服務層介面
└── OrdersServiceImpl.java // 服務層實現
└── repository/
└── OrdersRepository.java // 資料訪問層介面
└── JdbcOrdersRepository.java // 資料訪問層實現
特點:
- 按技術功能水平切分(Web、Service、Repository)
- 每層只依賴下一層
- 類似經典的三層架構
優點:
- 結構簡單,容易理解
- 適合小型專案快速開發
- 技術關注點分離清晰
缺點:
- 無法反映業務領域結構
- 當專案變大時難以維護
- 容易出現依賴跨層的情況
// 常見的錯誤:跨層調用
public class OrdersController {
@Autowired
private OrdersRepository repository; // 直接依賴 Repository 層,跳過 Service 層
}
2.2 Package by Feature(按功能分包)
com.company.myapp/
└── orders/
└── OrdersController.java // 所有訂單相關的類
└── OrdersService.java // 都放在同一個包中
└── OrdersRepository.java
└── users/
└── UsersController.java // 所有用戶相關的類
└── UsersService.java // 都放在同一個包中
└── UsersRepository.java
特點:
- 按業務功能垂直切分
- 相關的類都放在一起
- 體現了業務領域結構
優點:
- 容易找到相關程式碼
- 反映業務領域
- 降低跨包調用
缺點:
- 可能導致程式碼重複
// orders 套件
package com.company.orders;
class ValidationUtils {
static boolean validateEmail(String email) {
// email 驗證邏輯
}
}
// users 套件
package com.company.users;
class ValidationUtils { // 重複的驗證邏輯
static boolean validateEmail(String email) {
// 相同的 email 驗證邏輯
}
}
- 套件之間的界限可能不清晰
package com.company.orders;
public class OrderProcessor {
private UserService userService; // 應該屬於 users 套件?還是 orders 套件?
private PaymentService paymentService; // 支付相關是否應該獨立成套件?
public void processOrder(Order order) {
// 訂單處理邏輯與用戶、支付邏輯混合
}
}
- 共享組件的位置可能難以決定
// 這些工具類應該放在哪個套件?
package com.company.common; // 創建新套件?
// 或是
package com.company.orders; // 放在既有套件?
// 或是
package com.company.utils; // 獨立工具套件?
public class DateUtils {
public static String formatDate(Date date) {...}
}
2.3 Ports and Adapters(端口和適配器)
com.company.myapp/
└── domain/ // 核心業務邏輯(內部)
└── orders/
└── Orders.java // 領域模型
└── OrdersService.java // 業務邏輯
└── infrastructure/ // 技術實現(外部)
└── web/
└── OrdersController.java // Web 適配器
└── persistence/
└── JdbcOrdersRepository.java // 資料庫適配器
特點:
- 明確分離業務邏輯和技術實現
- 區分內部(domain)和外部(infrastructure)
- 使用領域術語進行命名
核心規則:
- 外部依賴內部
- 內部不知道外部的存在
- 使用依賴反轉原則
- 複雜度增加
- 需要更多的介面和轉接層
- 初期開發速度較慢
- 對團隊要求較高
// 一個簡單的查詢可能需要多層轉換
public interface OrderPort { // 領域端口
OrderDomain findOrder(String id);
}
public interface OrderRepository { // 持久化端口
OrderEntity findById(String id);
}
public class OrderAdapter implements OrderPort { // 轉接器
private final OrderRepository repo;
private final OrderMapper mapper;
@Override
public OrderDomain findOrder(String id) {
OrderEntity entity = repo.findById(id);
return mapper.toDomain(entity);
}
}
- 過度設計風險
- 小型專案可能不需要這麼多層級
- 可能導致「架構過度症」(Architecture Astronaut)
- 效能考量
// 多層轉換可能影響效能
@RestController
public class OrderController {
private final OrderPort orderPort;
public OrderDTO getOrder(String id) {
OrderDomain domain = orderPort.findOrder(id); // 第一次轉換
return mapper.toDTO(domain); // 第二次轉換
}
}
2.4 Package by Component(按組件分包)
com.company.myapp/
└── web/ // 所有 web 相關的控制器
└── OrdersController.java
└── components/
└── orders/
└── api/
└── OrdersComponent.java // 公開的介面
└── internal/ // 內部實現
└── OrdersBusinessLogic.java
└── OrdersDataAccess.java
特點:
- 將相關的業務邏輯和資料訪問封裝在一起
- 只暴露必要的公共介面
- 強調封裝性和組件化
優點:
- 更好的封裝性
- 清晰的組件邊界
- 可以通過編譯器強制執行架構規則
缺點:
- 元件粒度拿捏困難
// 太大的元件
package com.company.sales;
public interface SalesComponent { // 責任太多
void processOrder();
void handleShipping();
void manageInventory();
void handleReturns();
void processPayment();
}
// 太小的元件
package com.company.order.validation;
public interface OrderValidationComponent { // 過度切分
boolean validateOrderAmount();
}
- 跨元件溝通複雜化
public interface OrderComponent {
Order createOrder(OrderRequest request);
}
public interface InventoryComponent {
void updateStock(String productId, int amount);
}
// 跨元件協作變得複雜
@Service
public class OrderProcessor {
private final OrderComponent orderComponent;
private final InventoryComponent inventoryComponent;
private final PaymentComponent paymentComponent;
public void processOrder(OrderRequest request) {
// 需要協調多個元件的操作
Order order = orderComponent.createOrder(request);
inventoryComponent.updateStock(order.getProductId(), order.getAmount());
paymentComponent.process(order.getPaymentInfo());
}
}
- 可能導致隱性耦合
// OrderComponent 可能因為共享資料結構而與其他元件產生耦合
public interface OrderComponent {
void createOrder(OrderRequest request); // OrderRequest 可能包含其他元件的邏輯
}
public class OrderRequest {
private CustomerInfo customerInfo; // 來自 Customer 元件
private PaymentInfo paymentInfo; // 來自 Payment 元件
private ShippingInfo shippingInfo; // 來自 Shipping 元件
}
- 測試複雜度增加
@Test
public void testOrderCreation() {
// 需要模擬多個相依元件
OrderComponent orderComponent = mock(OrderComponent.class);
InventoryComponent inventoryComponent = mock(InventoryComponent.class);
PaymentComponent paymentComponent = mock(PaymentComponent.class);
// 設置期望行為變得複雜
when(orderComponent.createOrder(any())).thenReturn(new Order());
when(inventoryComponent.checkStock(any())).thenReturn(true);
// 測試邏輯...
}
3. 訪問修飾符的重要性
3.1 核心概念
訪問修飾符是實現封裝的關鍵工具,它決定了架構規則是否能被切實執行。
問題情境
想像一下這個場景:
// 所有類都是 public
public class OrdersController { ... }
public class OrdersService { ... }
public class OrdersRepository { ... }
public class OrdersServiceImpl { ... }
public class JdbcOrdersRepository { ... }
當所有類都是 public 時:
- 任何類都能被任意訪問
- 架構規則只能靠人為遵守
- 分包(package)失去意義,淪為純組織作用
這就像是:
- 蓋了一棟大樓,但所有門都不上鎖
- 設計了管制流程,但人人都有萬能鑰匙
- 規定要從一樓坐電梯,但樓梯間也不鎖門
3.2 正確的使用方式
A. 基本原則
package com.company.orders;
// 1. 對外開放的介面:public
public interface OrdersComponent {
void processOrder(Order order);
}
// 2. 內部實現:package-private(不加修飾符)
class OrdersBusinessLogic implements OrdersComponent {
// 3. 內部細節:private
private final OrdersRepository repository;
OrdersBusinessLogic(OrdersRepository repo) {
this.repository = repo;
}
@Override
public void processOrder(Order order) {
// 實現細節
}
}
B. 錯誤示例和修正
// 錯誤示例
package com.company.orders.service;
public class OrdersServiceImpl { // 錯誤:實現類不需要 public
public OrdersRepository repository; // 錯誤:字段不應該是 public
public void internalProcess() { ... } // 錯誤:內部方法不應該是 public
}
// 正確示例
package com.company.orders.service;
class OrdersServiceImpl { // 正確:package-private
private final OrdersRepository repository; // 正確:private
OrdersServiceImpl(OrdersRepository repo) { // 正確:package-private
this.repository = repo;
}
void internalProcess() { ... } // 正確:package-private
}
3.3 各架構方式的具體應用
A. Package by Layer
// web 包
public class OrdersController { ... } // public:需要接收外部請求
// service 包
public interface OrdersService { ... } // public:需要被 controller 使用
class OrdersServiceImpl { ... } // package-private:實現細節
// repository 包
public interface OrdersRepository { ... } // public:需要被 service 使用
class JdbcOrdersRepository { ... } // package-private:實現細節
B. Package by Feature
package com.company.orders;
public class OrdersController { ... } // public:對外接口
class OrdersService { ... } // package-private:內部服務
class OrdersRepository { ... } // package-private:內部存儲
C. Ports and Adapters
// domain 包
public interface Orders { ... } // public:核心領域接口
class OrdersService { ... } // package-private:領域邏輯
// infrastructure 包
public class WebController { ... } // public:外部適配器
class DatabaseRepository { ... } // package-private:實現細節
D. Package by Component
package com.company.orders;
// api 包
public interface OrdersComponent { ... } // public:組件對外接口
// internal 包
class OrdersLogic { ... } // package-private:內部實現
class OrdersData { ... } // package-private:內部實現
3.4 訪問修飾符帶來的好處
- 編譯時強制執行架構規則
// 這樣的程式碼將無法通過編譯
public class OrdersController {
private JdbcOrdersRepository repository; // 錯誤:無法訪問 package-private 類
}
- 防止意外依賴
// 這樣的跨層調用會在編譯時被阻止
public class WebLayer {
private DatabaseLayer db; // 錯誤:無法訪問其他包的 package-private 類
}
- 明確的封裝邊界
package com.company.orders;
public interface OrdersComponent { // 這是唯一可以被外部使用的類
// 公開的 API
}
// 所有實現細節都被封裝在包內
class Implementation1 { ... }
class Implementation2 { ... }
class Helper { ... }
3.5 實踐建議
- 設計原則:
- 預設使用最嚴格的訪問級別
- 只在必要時才開放訪問權限
- 優先使用 package-private
- 檢查清單:
- 是否真的需要將這個類設為 public?
- 這個方法需要被外部調用嗎?
- 這個字段可以設為 private 嗎?
- 常見錯誤:
- 習慣性地將所有類設為 public
- 為了單元測試而過度開放訪問權限
- 忽視 package-private 的封裝作用
通過正確使用訪問修飾符,我們可以:
- 讓編譯器幫助我們執行架構規則
- 提供更好的封裝性
- 防止架構被意外破壞
- 使程式碼更容易維護和重構
4. 進階解耦方式
4.1 模組系統
A. OSGi (Open Service Gateway initiative)
// OSGi bundle manifest 示例
Bundle-SymbolicName: com.mycompany.orders
Bundle-Version: 1.0.0
Export-Package: com.mycompany.orders.api
Import-Package: com.mycompany.common.api
- 允許運行時動態加載/卸載模組
- 精確控制模組間可見性
- 模組生命週期管理
B. Java 9 模組系統
// module-info.java
module com.mycompany.orders {
exports com.mycompany.orders.api;
requires com.mycompany.common;
provides OrderService with OrderServiceImpl;
}
- 明確定義模組邊界
- 區分 public 和 published 類型
- 通過 exports 控制可見性
4.2 源碼層面解耦
A. 完全分離方案
project-root/
├── domain/ # 業務領域程式碼
│ └── src/
│ └── OrdersService.java
├── web/ # Web 層程式碼
│ └── src/
│ └── OrdersController.java
└── persistence/ # 資料持久層程式碼
└── src/
└── JdbcOrdersRepository.java
B. 簡化的雙源碼樹方案
project-root/
├── domain/ # 內部程式碼(領域核心)
└── infrastructure/ # 外部程式碼(技術實現)
5. 常見陷阱:Périphérique 反模式
5.1 問題描述
- 名稱來源:巴黎的外環道路(可以繞過市中心)
- 基礎設施層的組件直接互相調用,繞過領域層
5.2 示例
// 錯誤示例
public class OrdersController {
@Autowired
private JdbcOrdersRepository repository; // 直接依賴其他基礎設施層程式碼
public void handleRequest() {
repository.findOrder(id); // 繞過領域層直接訪問資料庫
}
}
5.3 如何避免
- 強制通過領域層通信
- 正確使用訪問修飾符
- 定期架構審查
6. 如何選擇合適的架構方式?
6.1 小型專案(3-10人團隊)
- 建議:Package by Feature
- 原因:開發快速、維護簡單
6.2 中型專案(10-30人團隊)
- 建議:Package by Component + Java 9 Modules
- 原因:良好封裝、清晰邊界
6.3 大型專案(30人以上團隊)
- 建議:Ports and Adapters + 分離源碼樹
- 原因:高度模組化、嚴格架構控制
7. 結論
- 架構的成功在於實作細節
- 訪問修飾符是關鍵工具
- 根據團隊規模選擇架構
- 警惕架構反模式
- 保持架構的持續改進
- 找到適合團隊的平衡點