字數總計: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)
  • 使用領域術語進行命名

核心規則

  • 外部依賴內部
  • 內部不知道外部的存在
  • 使用依賴反轉原則
  1. 複雜度增加
  • 需要更多的介面和轉接層
  • 初期開發速度較慢
  • 對團隊要求較高
// 一個簡單的查詢可能需要多層轉換
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);
    }
}
  1. 過度設計風險
  • 小型專案可能不需要這麼多層級
  • 可能導致「架構過度症」(Architecture Astronaut)
  1. 效能考量
// 多層轉換可能影響效能
@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

特點

  • 將相關的業務邏輯和資料訪問封裝在一起
  • 只暴露必要的公共介面
  • 強調封裝性和組件化

優點

  • 更好的封裝性
  • 清晰的組件邊界
  • 可以通過編譯器強制執行架構規則

缺點

  1. 元件粒度拿捏困難
// 太大的元件
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();
}
  1. 跨元件溝通複雜化
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());
    }
}
  1. 可能導致隱性耦合
// OrderComponent 可能因為共享資料結構而與其他元件產生耦合
public interface OrderComponent {
    void createOrder(OrderRequest request);  // OrderRequest 可能包含其他元件的邏輯
}

public class OrderRequest {
    private CustomerInfo customerInfo;  // 來自 Customer 元件
    private PaymentInfo paymentInfo;   // 來自 Payment 元件
    private ShippingInfo shippingInfo; // 來自 Shipping 元件
}
  1. 測試複雜度增加
@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 時:

  1. 任何類都能被任意訪問
  2. 架構規則只能靠人為遵守
  3. 分包(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 訪問修飾符帶來的好處

  1. 編譯時強制執行架構規則
// 這樣的程式碼將無法通過編譯
public class OrdersController {
    private JdbcOrdersRepository repository; // 錯誤:無法訪問 package-private 類
}
  1. 防止意外依賴
// 這樣的跨層調用會在編譯時被阻止
public class WebLayer {
    private DatabaseLayer db; // 錯誤:無法訪問其他包的 package-private 類
}
  1. 明確的封裝邊界
package com.company.orders;

public interface OrdersComponent {  // 這是唯一可以被外部使用的類
    // 公開的 API
}

// 所有實現細節都被封裝在包內
class Implementation1 { ... }
class Implementation2 { ... }
class Helper { ... }

3.5 實踐建議

  1. 設計原則
    • 預設使用最嚴格的訪問級別
    • 只在必要時才開放訪問權限
    • 優先使用 package-private
  2. 檢查清單
    • 是否真的需要將這個類設為 public?
    • 這個方法需要被外部調用嗎?
    • 這個字段可以設為 private 嗎?
  3. 常見錯誤
    • 習慣性地將所有類設為 public
    • 為了單元測試而過度開放訪問權限
    • 忽視 package-private 的封裝作用

通過正確使用訪問修飾符,我們可以:

  1. 讓編譯器幫助我們執行架構規則
  2. 提供更好的封裝性
  3. 防止架構被意外破壞
  4. 使程式碼更容易維護和重構

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 如何避免

  1. 強制通過領域層通信
  2. 正確使用訪問修飾符
  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. 結論

  1. 架構的成功在於實作細節
  2. 訪問修飾符是關鍵工具
  3. 根據團隊規模選擇架構
  4. 警惕架構反模式
  5. 保持架構的持續改進
  6. 找到適合團隊的平衡點