4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クリーンアーキテクチャとオニオンアーキテクチャの違い

Last updated at Posted at 2024-10-16

ドメイン駆動設計で一緒に用いられるアーキテクチャ、奥が深い。
そりゃ皆が色々本を出すわけですよ…。

今回は表題の通り、クリーンアーキテクチャとオニオンアーキテクチャの違い、二つのアーキテクチャ名はよく見るけど、ソースで見たら具体的にどう変わるの?ってのを調べたのでメモ。

二つの違い

まずは有名な各アーキテクチャの図を見てみる。

cleanarch.jpg

onionarch.png

次に、文言で二つの違いを書いてみる

クリーンアーキテクチャ
クリーンアーキテクチャは円構造、以下の4つのレイヤーを持つ

  • エンティティ層
     アプリケーションのビジネスルールや状態を表すモデル
  • ユースケース層
     特定のユースケースに従ってエンティティを操作する
  • インターフェース層
     外部への入出力(インフラ、UI)とのやり取り
  • インフラ層
     DB、ファイルシステム、外部サービスとの接続

図にEntitiesと書かれているのでEntityしか入れちゃダメなの?となりそうだがそういう訳ではない事に注意。
Entitiesに入れて良いものは以下の通り

名前 理由
Entity 識別子を持ちドメイン内で変化する物(ユーザー情報等)
ValueObject 振る舞いと等価性で表現される値
DomainService 1つのEntityだけでは表現できないドメインロジック
ドメインに属するinterface 外部に依存しない抽象化
ポリシーに関するinterface 認可・判断などのドメイン的な抽象ルール

Q. 図には Interface Adapterって書かれてますけど?
A. このInterface Adapterってのは、interfaceの実装クラスです。

具体的にはこんな感じ。ValueObject(VO)も使っていいものとして書いたのでVOを使ったサンプル

// Domain層(ValueObject)
final class UserId
{
    private string $value;

    public function __construct(string $value) {
        // UUID形式などをバリデーションしても良い
        $this->value = $value;
    }

    public function equals(UserId $other): bool {
        return $this->value === $other->value;
    }

    public function getValue(): string {
        return $this->value;
    }
}

// Domain層
interface IUserRepository
{
    public function getUserId(UserId $userId): ?User;
    public function save(User $user): bool;
}

// Interface Adapter(インフラ層)
class UserRepository implements IUserRepository
{
    public function getUserId(UserId $userId): ?User
    {
        $userInfo = DB::table('users')->findById($userId->getValue();

        if( is_null($userInfo) ) return null;

        return new User(
            new UserId($userIndo->id),
            ...ダルいので略
        );
    }

    public function save(User $user): bool
    {
        // 手抜き
        // !!は結果を2回NOT(結果が0 ->TRUE->FALSE, 結果が1以上 ->FALSE->TRUE)へ
        return !!(new App\Models\User())->fill($user->toArray())->save();
    }
}

VOはドメイン層のクラスなので「ドメイン知識の流出だ!」ってなるかもだけど、外側から内側に依存してる分には問題ないです念のため。気になるならVOもinterface書けば?(投げやり)

クリーンアーキテクチャ一つで本が一冊書けるくらいめんどくs情報があるのでとりあえずそろそろオニオンアーキテクチャについて書く。

オニオンアーキテクチャ
同じくレイヤー構造を持つが、ドメインモデル(ビジネスルール)を中心に捉えた構造

  • ドメインモデル
     ビジネスのコア。ドメインというキーワード自体はドメインサービスも含むがここはinterfaceやEntityなどが焦点。
  • ドメインサービス
     ビジネスに関するロジック処理を記述する層
  • アプリケーションサービス
     ユースケースやアプリケーションサービスが属し、ドメインモデルを利用してビジネスロジックを実行
  • 外部依存層
     レポジトリや外部のインフラに関する依存が存在し、最外層に配置

こっちは明確にドメインが大きく二つに分かれてる。
結論的な事は最後にしたかったけど、ここで書かないといけなくなりそうなので、とりあえず、一回ソース書く。

ソースサンプル

前回 みたいに簡単すぎるサンプルだと差異がないじゃん…ってなりそうなので、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 "確認メールを送信しました。";
    }
}

特徴

  • ビジネスロジックがユースケースに集中し、エンティティは単純なデータ保持(Entity, ValueObject)や簡単なロジック(interface)を持つ。
  • 各ユースケースに応じて、複数のサービスを利用してビジネスロジックを実行する

オニオンアーキテクチャ

<?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 "確認メールを送信しました。";
    }
}

特徴

  • ビジネスロジックはドメインサービスで実行され、注文作成に必要な処理がサービスに集約される。
  • エンティティはデータを保持するだけで、ロジック自体(interface, Service)はドメインサービスにある

ざっくり書くと、クリーンアーキテクチャはユースケース単位でクラスを作成し、ユースケース層でEntityの生成や操作、永続化を行う。
ドメインサービスを無駄に作ろうとしない。誤解を恐れず書くと、基本的にユースケースに書けばいいじゃんって考え方。

オニオンアーキテクチャはアプリケーションサービスにはUserManagerみたいな、ある情報を管理するクラスがあって、そのクラスが登録だったり更新だったりのサービス(メソッド)を提供していて、そのクラスは更にドメインサービスという機能を複数呼び出したり組み合わせてサービスを提供している感じ。

そしてサンプル見て気付いた方はいると思うけど、オニオンアーキテクチャはドメインサービスとドメインモデルが外側に依存する事は絶対禁止である一方、アプリケーションサービスが外側の層に依存するのは許容される範囲(その分載せ替えの時大変だけど)

二つのアーキテクチャの違い

他にもいろいろ書きたい事あるけど、とりあえず。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?