ドメイン駆動設計で一緒に用いられるアーキテクチャ、奥が深い。
そりゃ皆が色々本を出すわけですよ…。
今回は表題の通り、クリーンアーキテクチャとオニオンアーキテクチャの違い、二つのアーキテクチャ名はよく見るけど、ソースで見たら具体的にどう変わるの?ってのを調べたのでメモ。
二つの違い
まず、文言で二つの違いをおさらい
クリーンアーキテクチャ
クリーンアーキテクチャは円構造、以下の4つのレイヤーを持つ
-
エンティティ層
アプリケーションのビジネスルールや状態を表すモデル -
ユースケース層
特定のユースケースに従ってエンティティを操作する -
インターフェース層
外部への入出力(インフラ、UI)とのやり取り -
インフラ層
DB、ファイルシステム、外部サービスとの接続
オニオンアーキテクチャ
同じくレイヤー構造を持つが、ドメインモデル(ビジネスルール)を中心に捉えた構造
-
ドメインモデル
ビジネスのコアになる。エンティティとドメインサービスを含む -
アプリケーションサービス
ユースケースやアプリケーションサービスが属し、ドメインモデルを利用してビジネスロジックを実行 -
外部依存層
レポジトリや外部のインフラに関する依存が存在し、最外層に配置
次に各アーキテクチャの図を見てみる。
さて、二つのアーキテクチャ別でソースを記載してみる。
ソースサンプル
前回 みたいに簡単すぎるサンプルだと差異がないじゃん…ってなりそうなので、ECサイトを例にして書いてみる。
クリーンアーキテクチャ
<?php
// エンティティ(ドメイン層)
namespace Package\Domain\Entity;
class Order {
private $items = [];
public function addItem($item): void {
$this->items[] = $item;
}
public function getItems(): array {
return $this->items;
}
}
//----------------------------------------------------------------------
// ユースケース層
namespace Package\UseCase;
class CreateOrderUseCase {
public function __construct(
private InventoryChecker $inventoryService,
private EmailServiceInterface $emailService
) {
}
public function execute($order) {
// 在庫確認
foreach ($order->getItems() as $item) {
if (!$this->inventoryService->checkStork($item)) {
throw new Exception('在庫不足');
}
}
// その他、注文処理(決済処理、在庫数変更など)
// 注文処理成功後にメール送信
$this->emailService->send($order);
}
}
//----------------------------------------------------------------------
/* これらはシステムの機能を提供するサービスのインターフェース。
* アプリケーションロジックやインフラロジックに該当する。
* クリーンアーキテクチャの場合、このインターフェースはドメイン層かアプリケーション層に属する
* (例:エンティティが参照するサービスならドメイン層)
*/
namespace Package\Application\Service;
interface InventoryChecker {
public function checkStock($item): bool;
}
interface SendConfirmationInterface {
public function send($order): bool;
}
//----------------------------------------------------------------------
// インフラ層
namespace Package\Infra;
class InventoryService implements InventoryChecker{
public function checkStock($item): bool {
// 在庫確認ロジック
return true;
}
}
class EmailService implements SendConfirmationInterface {
public function send($order): bool {
// メール送信処理
echo "確認メールを送信しました。";
}
}
特徴
- ビジネスロジックがユースケースに集中し、エンティティは単純なデータ保持や簡単なロジックを持つ。
- 各ユースケースに応じて、複数のサービスを利用してビジネスロジックを実行する
オニオンアーキテクチャ
<?php
// ドメイン層
namespace Package\Domain\Entity;
class Order {
private $items = [];
public function addItem($item): void {
$this->items[] = $item;
}
public function getItems(): array {
return $this->items[];
}
}
//----------------------------------------------------------------------
// ドメインサービス(ビジネスロジック)
namespace Package\Domain\Service;
class OrderService {
public function __construct(
private InventoryChecker $inventoryService,
private SendConfirmationInterface $emailService
) {
}
public function createOrder(Order $order) {
// 在庫チェック
foreach ($order->getItems() as $item) {
if (!$this->inventoryService->checkStock($item)) {
throw new Exception('在庫不足');
}
}
// その他、注文処理(決済処理、在庫数変更など)
$this->emailService->send($order);
}
}
//----------------------------------------------------------------------
// アプリケーションサービス層
namespace Package\Application\Service;
use Package\Domain\Service\OrderService;
use Package\Infra\UnitOfWork; // アプリケーション層がインフラ層に依存するのは許容される範囲
use Package\Domain\Repository\OrderRepositoryInterface; //ホントはこうする
class CeateOrderApplicationService {
public function __construct(
private OrderService $orderService,
private OrderRepositoryInterface $orderRepository,
private UnitOfWork $unitOfWork
){
}
public function execute(CreateOrderRequest $request): CreateOrderResponse {
$this->unitOfWork->beginTransaction();
try {
// ビジネスロジックを呼び出す
$order = $this->orderService->createOrder($request->items);
// 注文の保存
$this->orderRepository->save($order);
// トランザクションのコミット
$this->unitOfWork->commit();
return new CreateOrderResponse($order);
} catch (Exception $e) {
$this->unitOfWork->rollback();
throw $e;
}
}
}
//----------------------------------------------------------------------
/* これらはシステムの機能を提供するサービスのインターフェース。
* アプリケーションロジックやインフラロジックに該当する。
* オニオンアーキテクチャではこれらのインターフェースはドメイン層に属する
*/
namespace Package\Application\Service;
interface InventoryChecker {
public function checkStock($item): bool;
}
interface SendConfirmationInterface {
public function send($order): bool;
}
//----------------------------------------------------------------------
// インフラ層
namespace Package\Infra;
class InventoryService implements InventoryChecker{
public function checkStock($item): bool {
// 在庫確認ロジック
return true;
}
}
class EmailService implements SendConfirmationInterface {
public function send($order): bool {
// メール送信処理
echo "確認メールを送信しました。";
}
}
特徴
- ビジネスロジックはドメインサービスで実行され、注文作成に必要な処理がサービスに集約される。
- エンティティはデータを保持するだけで、ロジック自体はドメインサービスにある
サンプルソースでも書いたけど、オニオンアーキテクチャのアプリケーション層がインフラ層に依存するのは許される範囲らしい。クリーンアーキテクチャに慣れた自分からするとマジかよって感じだけど。
大事なのはドメイン層が内側から外側に依存しない作りって事。
二つのアーキテクチャの違い
まとめるとこうなる。
クリーンアーキテクチャ
- ユースケース層がビジネスロジックを担い、その下にエンティティ層(いわゆるドメイン)がある
- ユースケースはエンティティを操作する必要があるので、エンティティより上位の層にある
オニオンアーキテクチャ
- ドメインモデル(エンティティやサービス)が中心になる
- ドメインモデルが他の全ての層に依存しない
- 他のレイヤーはドメインのサポートをするために存在する
他にもいろいろ書きたい事あるけど、とりあえず。
DTO、VOについても書きたい。クリーンアーキテクチャなのに外側の層に属するハズのDTOをアプリケーションサービス(ユースケース層)に普通に投げ入れている実装をたまに見かけるので。