2025年6月17日 星期二

如何在 Laravel 中實踐領域驅動設計 (DDD) 的部分概念,特別是領域層、應用程式層和基礎設施層的分離

構建企業級 Laravel 應用:DDD 與六邊形架構的實戰指南

嗨,程式碼愛好者們!今天,我想和大家分享一個我在 Laravel 專案中實踐 Domain-Driven Design (DDD) 和六邊形架構 (Hexagonal Architecture) 的經驗。許多開發者都曾面臨如何在 Laravel 的快速開發優勢下,同時打造出易於維護、可擴展且業務邏輯清晰的企業級應用。這篇文章將揭示我們的解決方案。

為什麼選擇 DDD 和六邊形架構?

在傳統的 MVC 架構中,隨著業務邏輯的複雜化,控制器 (Controller) 和模型 (Model) 往往會變得臃腫,導致「肥控制器,瘦模型」或「肥模型」問題,使業務邏輯與框架細節緊密耦合,難以測試和迭代。

1. 分層架構圖 (Layered Architecture Diagram)

這個圖著重於展示不同層次的劃分以及它們之間的依賴方向。


2. 訂單流程圖 (Order Placement Flowchart)

這個圖更側重於「下訂單」這個特定用例的執行流程和步驟。



DDD 和六邊形架構為此提供了強大的解藥:

  • 領域驅動設計 (DDD):將焦點放在複雜的業務領域本身。通過 Ubiquitous Language(通用語言)、實體 (Entities)、值物件 (Value Objects)、聚合 (Aggregates)、領域服務 (Domain Services) 和領域事件 (Domain Events) 等概念,我們能更好地理解業務,並將其轉化為清晰、內聚的程式碼。
  • 六邊形架構 (Hexagonal Architecture / Ports & Adapters):這是一種架構模式,旨在將應用程式的核心業務邏輯與外部技術(如資料庫、Web 框架、API 等)解耦。核心應用透過「埠 (Ports)」暴露其功能,而外部技術則透過「適配器 (Adapters)」連接到這些埠。這意味著我們的業務邏輯可以獨立於任何基礎設施而存在和測試。

結合這兩者,我們可以在 Laravel 中構建出一個堅固、靈活且面向未來的應用程式。

我們的專案結構:一次「下訂單」的旅程

為了具體說明,我將以一個電商系統中的「下訂單」流程為例,展示如何將這些原則應用到實際的 Laravel 程式碼中。

整個流程可以從一個外部請求(如前端的 API 呼叫)開始,穿透不同的層次,最終完成業務操作並返回響應。

1. 介面層 (Interface Layer / UI Layer)

這是使用者與應用程式互動的地方,主要處理 HTTP 請求和響應。

  • routes/api.php: 定義 API 路由,將 /api/orders 的 POST 請求路由到 OrderController@store
  • app/Http/Requests/PlaceOrderRequest.php: 使用 Laravel Form Request 進行輸入驗證,確保請求數據的有效性和完整性(例如,購物車商品列表及其數量)。這使得控制器保持精簡。
  • app/Http/Controllers/OrderController.php: 作為一個薄控制器,它的職責是:
    • 接收 PlaceOrderRequest 驗證後的數據。
    • 從認證系統獲取 customer_id (實際應用中)。
    • 將請求數據轉換為應用層的命令 (Command)PlaceOrderCommand
    • 將命令派遣給應用層的命令處理器 (Command Handler) 進行處理。
    • 捕獲潛在的業務異常,並將結果(如 OrderCreatedDTO)轉換為標準的 API 響應格式(使用 OrderResource)。
  • app/Http/Resources/OrderResource.php: 將應用層返回的 DTO(OrderCreatedDTO)轉換為標準的 JSON 響應格式,確保 API 輸出的統一性。
PHP
// app/Http/Controllers/OrderController.php (部分代碼)
public function store(PlaceOrderRequest $request): JsonResponse
{
    $customerId = 'user-123'; // 實際應從 Auth::user()->id 獲取
    $command = new PlaceOrderCommand($customerId, $request->input('cart_items'));

    try {
        $orderDto = $this->placeOrderCommandHandler->handle($command);
        return new JsonResponse([
            'message' => 'Order placed successfully.',
            'order'   => new OrderResource($orderDto)
        ], 201);
    } catch (\App\Exceptions\InsufficientStockException $e) {
        // 自定義錯誤處理
        return response()->json(['message' => $e->getMessage()], 409); // 409 Conflict
    } catch (\RuntimeException $e) {
        // 其他 RuntimeException,如產品未找到
        return response()->json(['message' => $e->getMessage()], 404);
    }
}

2. 應用程式層 (Application Layer)

這一層負責協調領域邏輯,處理用例 (Use Cases)。它不包含業務規則,而是將請求(命令)轉換為對領域物件的操作。

  • app/Application/Commands/PlaceOrderCommand.php: 一個不可變的命令物件 (Command DTO),封裝了執行「下訂單」操作所需的所有輸入數據。
  • app/Application/Handlers/PlaceOrderCommandHandler.php: 命令處理器,它是應用層的核心:
    • 接收 PlaceOrderCommand
    • 協調領域服務 (OrderPlacementService) 執行核心業務邏輯。
    • 調用領域倉庫 (Repository) (OrderRepositoryInterface) 進行數據持久化。
    • 最關鍵的是,它負責管理事務 (Database Transaction),確保整個操作的原子性。
    • 發布領域事件 (Domain Events),如 OrderPlaced,以便後續的業務處理(如發送郵件、更新庫存記錄等)。
    • 返回一個數據傳輸物件 (DTO) (OrderCreatedDTO),將結果返回給介面層,避免直接暴露領域實體。
PHP
// app/Application/Handlers/PlaceOrderCommandHandler.php (部分代碼)
public function handle(PlaceOrderCommand $command): OrderCreatedDTO
{
    return DB::transaction(function () use ($command) {
        // 執行核心業務邏輯 (由領域服務負責)
        $order = $this->orderPlacementService->createOrder($command->customerId, $command->cartItems);

        // 持久化訂單 (由基礎設施層的 Repository 實作)
        $this->orderRepository->save($order);

        // 發布所有在領域實體中記錄的領域事件
        foreach ($order->releaseEvents() as $event) {
            $this->eventDispatcher->dispatch($event);
        }

        return new OrderCreatedDTO($order->getId(), $order->getTotalAmount());
    });
}
  • app/Application/DTOs/OrderCreatedDTO.php: 一個簡單的數據結構,用於在應用層和介面層之間傳遞簡化的訂單創建結果。

3. 領域層 (Domain Layer)

這是應用程式的心臟,包含所有核心業務邏輯、規則和實體。它獨立於任何技術細節,是整個應用程式中最「純粹」的部分。

  • app/Domain/Entities/Order.php: 訂單領域實體。它不僅包含訂單的屬性(ID、客戶ID、狀態、總金額、訂單項),還封裝了業務行為(例如,calculateTotalAmount()markAsPaid()decreaseStock() 等)。它還負責記錄領域事件
    • private __construct() 和靜態工廠方法 create() 確保實體的創建符合業務規則。
    • 新增 fromPersistence() 靜態方法,用於從資料庫數據重建實體,避免 Repository 層的反射操作,提高可讀性和健壯性。
  • app/Domain/Entities/OrderItem.php: 訂單項領域實體,包含產品信息和數量。
  • app/Domain/Entities/Product.php: 產品領域實體,包含產品的名稱、價格和庫存,以及庫存扣減的業務行為 (decreaseStock())。
  • app/Domain/Services/OrderPlacementService.php: 領域服務,處理跨多個實體的業務邏輯,例如在創建訂單時檢查產品庫存並扣減。它依賴於 ProductRepositoryInterface 而不是具體的實現。
  • app/Domain/Repositories/OrderRepositoryInterface.phpapp/Domain/Repositories/ProductRepositoryInterface.php: 埠 (Ports)。這些是純粹的 PHP 介面,定義了領域層對外部持久化機制的依賴契約。領域層只知道它需要一個能夠「保存訂單」或「查找產品」的服務,而不知道具體如何實現。
  • app/Domain/Events/OrderPlaced.php: 領域事件,表示「訂單已成功下單」這個業務事實。它包含事件的上下文數據,可供其他部分響應。
PHP
// app/Domain/Entities/Order.php (部分代碼)
class Order
{
    // ... 屬性定義

    private function __construct(string $customerId, Collection $items) { /* ... */ }

    public static function create(string $customerId, Collection $items): self
    {
        $order = new self($customerId, $items);
        $order->recordEvent(new OrderPlaced($order->id, $order->customerId, $order->totalAmount));
        return $order;
    }

    // 從持久化數據重建 Order 領域實體,由 Repository 調用
    public static function fromPersistence(
        string $id, string $customerId, Collection $items,
        float $totalAmount, string $status, string $createdAt
    ): self {
        $order = new self($customerId, $items);
        // ... 通過反射或內部邏輯設置私有屬性
        return $order;
    }

    public function recordEvent(object $event): void { /* ... */ }
    public function releaseEvents(): array { /* ... */ }
    // ... 其他業務行為和 Getter
}

// app/Domain/Entities/Product.php (部分代碼)
class Product
{
    // ... 屬性定義
    public function decreaseStock(int $quantity): void
    {
        if ($this->stock < $quantity) {
            throw new \App\Exceptions\InsufficientStockException("Insufficient stock for product " . $this->name);
        }
        $this->stock -= $quantity;
    }
}

4. 基礎設施層 (Infrastructure Layer)

這一層負責實現領域層定義的埠,將外部技術(如資料庫、消息隊列、外部服務等)連接到應用程式核心。

  • app/Infrastructure/Persistence/Models/Order.phpProduct.phpOrderItem.php: Eloquent ORM 模型。這些模型是資料庫的直接映射,僅處理數據的 CRUD 操作和關聯,不包含任何業務邏輯。它們是資料庫適配器的一部分。為了避免與領域實體名稱衝突,我們將其明確命名為 Order (Eloquent Model 在命名空間中與領域實體區分開)。
  • app/Infrastructure/Persistence/Eloquent/EloquentOrderRepository.phpEloquentProductRepository.php: 適配器 (Adapters)。它們實現了 OrderRepositoryInterfaceProductRepositoryInterface 介面,使用 Eloquent ORM 與資料庫交互。它們負責將 Eloquent 模型轉換為領域實體,並將領域實體轉換為 Eloquent 模型進行持久化。
    • findByIds 中加入 lockForUpdate() 處理併發讀寫,確保庫存操作的原子性。
  • app/Providers/AppServiceProvider.php: 在 Laravel 的服務容器中,我們將領域層定義的介面 (OrderRepositoryInterface) 綁定到基礎設施層的具體實現 (EloquentOrderRepository)。這就是依賴注入的實踐,確保了領域層對基礎設施層的解耦。
  • app/Providers/EventServiceProvider.php: 註冊領域事件的監聽器。當 OrderPlaced 事件被發布時,SendOrderConfirmationEmail 監聽器將被觸發。
  • app/Infrastructure/Events/Listeners/SendOrderConfirmationEmail.php: 一個具體的監聽器範例,響應 OrderPlaced 事件,用於發送訂單確認郵件。它可以實現 ShouldQueue 介面,將耗時操作推送到隊列中異步執行。
PHP
// app/Infrastructure/Persistence/Eloquent/EloquentProductRepository.php (部分代碼)
public function findByIds(array $productIds): Collection
{
    // 使用 forUpdate 確保讀取時鎖定行,防止並發問題
    $eloquentProducts = EloquentProductModel::whereIn('id', $productIds)->lockForUpdate()->get();
    return $eloquentProducts->map(fn($p) => new DomainProduct($p->id, $p->name, $p->price, $p->stock));
}

// app/Infrastructure/Events/Listeners/SendOrderConfirmationEmail.php (部分代碼)
class SendOrderConfirmationEmail implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        Log::info("Sending order confirmation email for Order ID: {$event->orderId}");
        // ... 實際發送郵件邏輯
    }
}

5. 異常處理 (Exception Handling)

一個健壯的應用程式需要良好的異常處理機制。我們通過自定義異常類並在 app/Exceptions/Handler.php 中統一處理,實現了更精細的錯誤響應。

  • app/Exceptions/InsufficientStockException.php: 自定義的業務異常,用於表示庫存不足。
  • app/Exceptions/Handler.php: Laravel 的全局異常處理器。我們在此處捕獲 InsufficientStockException 和其他運行時異常,並將其轉換為適當的 HTTP 狀態碼和結構化的 JSON 錯誤響應(例如 409 Conflict 或 404 Not Found),增強 API 的健壯性和易用性。
PHP
// app/Exceptions/Handler.php (部分代碼)
public function render($request, Throwable $exception)
{
    if ($exception instanceof InsufficientStockException) {
        return new JsonResponse([
            'message' => 'Order cannot be placed due to insufficient stock.',
            'error'   => $exception->getMessage(),
            'code'    => 'INSUFFICIENT_STOCK',
        ], 409);
    }
    // ... 其他異常處理
    return parent::render($request, $exception);
}

專案部署與基礎設施即程式碼 (IaC)

為了實現企業級應用的可重複部署和擴展性,我們提倡使用 IaC。雖然範例程式碼沒有直接包含 IaC 配置,但其架構設計是完全兼容的。例如,使用 Terraform 可以定義和管理所有必要的雲基礎設施資源(如 AWS RDS 資料庫、ECS 容器服務、負載均衡器等)。

這確保了開發、測試和生產環境的一致性,並加速了部署流程。

測試策略

DDD 和六邊形架構的最大好處之一是可測試性

  • 單元測試:領域層和應用程式層的程式碼是純粹的 PHP,不依賴框架或資料庫,這使得它們非常適合進行快速、獨立的單元測試。我們可以輕鬆地模擬 (mock) 介面來測試核心業務邏輯。
  • 整合測試/功能測試:利用 Laravel 提供的測試工具,測試不同層次之間的協同工作,確保整個流程按預期運行。

總結

在 Laravel 中實踐 DDD 和六邊形架構,能夠幫助我們構建出更加健壯、可維護和可擴展的企業級應用程式。它強制我們將業務核心與基礎設施分離,使得業務邏輯更加清晰,團隊協作更加高效,並且能夠從容應對未來不斷變化的業務需求。

雖然引入這些模式會帶來一定的學習曲線和初始複雜度,但從長遠來看,它將極大地降低維護成本,提升開發效率,並為您的專案打下堅實的基礎。

專案 GitHub 連結:https://github.com/BpsEason/laravel-ddd-hexagonal-api.git

沒有留言:

張貼留言

網誌存檔