構建企業級 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 輸出的統一性。
// 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
),將結果返回給介面層,避免直接暴露領域實體。
- 接收
// 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.php
和app/Domain/Repositories/ProductRepositoryInterface.php
: 埠 (Ports)。這些是純粹的 PHP 介面,定義了領域層對外部持久化機制的依賴契約。領域層只知道它需要一個能夠「保存訂單」或「查找產品」的服務,而不知道具體如何實現。app/Domain/Events/OrderPlaced.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.php
、Product.php
、OrderItem.php
: Eloquent ORM 模型。這些模型是資料庫的直接映射,僅處理數據的 CRUD 操作和關聯,不包含任何業務邏輯。它們是資料庫適配器的一部分。為了避免與領域實體名稱衝突,我們將其明確命名為Order
(Eloquent Model 在命名空間中與領域實體區分開)。app/Infrastructure/Persistence/Eloquent/EloquentOrderRepository.php
和EloquentProductRepository.php
: 適配器 (Adapters)。它們實現了OrderRepositoryInterface
和ProductRepositoryInterface
介面,使用 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
介面,將耗時操作推送到隊列中異步執行。
// 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 的健壯性和易用性。
// 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 和六邊形架構,能夠幫助我們構建出更加健壯、可維護和可擴展的企業級應用程式。它強制我們將業務核心與基礎設施分離,使得業務邏輯更加清晰,團隊協作更加高效,並且能夠從容應對未來不斷變化的業務需求。
雖然引入這些模式會帶來一定的學習曲線和初始複雜度,但從長遠來看,它將極大地降低維護成本,提升開發效率,並為您的專案打下堅實的基礎。
沒有留言:
張貼留言