ドメイン駆動設計で一緒に用いられるアーキテクチャ、奥が深い。
そりゃ皆が色々本を出すわけですよ…。
今回は表題の通り、クリーンアーキテクチャとオニオンアーキテクチャの違い、二つのアーキテクチャ名はよく見るけど、ソースで見たら具体的にどう変わるの?ってのを調べたのでメモ。
二つの違い
まずは有名な各アーキテクチャの図を見てみる。
次に、文言で二つの違いを書いてみる
クリーンアーキテクチャ
クリーンアーキテクチャは円構造、以下の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みたいな、ある情報を管理するクラスがあって、そのクラスが登録だったり更新だったりのサービス(メソッド)を提供していて、そのクラスは更にドメインサービスという機能を複数呼び出したり組み合わせてサービスを提供している感じ。
そしてサンプル見て気付いた方はいると思うけど、オニオンアーキテクチャはドメインサービスとドメインモデルが外側に依存する事は絶対禁止である一方、アプリケーションサービスが外側の層に依存するのは許容される範囲(その分載せ替えの時大変だけど)
二つのアーキテクチャの違い
他にもいろいろ書きたい事あるけど、とりあえず。