クリーンアーキテクチャにおけるドメイン層の理想的な役割 ─ 依存関係とバリデーション責任
✨はじめに
複数のフレームワークを併用して開発しているシステムにおいては、明確な実装ルールが定められていない場合、アーキテクチャの混在や実装の不透明化が発生しやすい傾向があり、私自身が理想と考える実装の指針を整理して開発作業時に基準として意識を持つよう、まとめた資料を作成しました。
前職で個人的にDDD(Domain-Driven Design:ドメイン駆動設計)アーキテクチャにも興味がありましたが、単独で商用のシステムに適用するには課題が多いと印象があったり、SOLID原則やAndroid開発アーキテクチャ等もありますが、私は特に 「ドメイン層を軸に整理する」という考えと以前経験したプロジェクトの実装方法に近いクリーンアーキテクチャをベースにして、ドメイン層として責務や依存関係のあり方、バリデーションの責任範囲などに焦点を当てて整理してみました。
1) 依存関係(サービス層 ↔ ドメイン層)⚙️
原則(依存方向)
先ずは依存関係についてです。
- 上位(Application/Service層)→ ドメイン層 に依存する。
- ドメイン層 → 他層には依存しない(フレームワーク・DB・外部APIに非依存)。
- インフラは ドメインが定義したポート(インターフェース) を実装する。
ドメイン層が外部に依存しない設計・実装の要は 依存性逆転の原則(DIP) をきちんと守ることです。
ドメインが持つもの
クリーンアーキテクチャで言われているドメイン層で持つものな以下の要素があります。
- Entities / Aggregates / Value Objects / Domain Services / Policies / Domain Events
- Ports(外部依存を抽象化するインターフェース)
基本的にクリーンアーキテクチャにとってPortsはインフラ層にも依存関係を持たないことが可能にする要素であります。SpringのDIでのInterface使い方とは目的が異なるが、SpringのDIを使用しつつクリーンアーキテクチャを適用しようとしたプロジェクトもあるというかむしろ多かった認識です。
アプリケーションサービスが持つもの
本記事ではドメイン層をメインで考えたいことですが、バリデーションの責任範囲を考えるために一応書いてみました。
- ユースケースの手順(オーケストレーション)
- トランザクション境界の開始/終了
- DTO ↔ ドメインのマッピング
- 認可・監査ログ・外部I/Oの順序制御
判断基準
- 不変条件・計算はドメイン。
- 手続き・順序・I/Oはサービス。
- 外部API連携は Portを定義し、実装はインフラ層に任せる。
2) ドメイン層のバリデーションの範囲・責任 🧭
エンジニアの最初頃に個人的に一番迷う点だったと思います。複雑なプロジェクトで特にバリデーションの範囲を決めることが難しい時が多かったです。複数のチーム・メンバーで作業して似たバリデーションが違う層で複数存在したりした経験もあります。そもそもドメインの決定(分割)からが大事だと思って、間違えるとバリデーションの責任にも影響されます。
因みに、他の層を含めてバリデーションの棲み分けを簡単に考えてみました。
- サービス層:形式的バリデーション(型・必須・フォーマット)、認可。
- ドメイン層:意味的バリデーション(不変条件、ビジネス上の妥当性)。
- インフラ層:技術的制約(DBのNOT NULL/Unique、外部キー、スキーマ制約)。
例(Value Objectで不変条件を保証)
プログラムの処理シーケンスの間、その真理値が常に真である述語(条件)を指します。例えば以下のサンプルを見ると数量というのはどの処理中でも0より少なくはならないことを保証する必要があります。
public record Quantity(int value) {
public Quantity {
if (value < 0) throw new DomainException("quantity must be > 0");
}
}
例(Aggregateで状態遷移を保証)
状態がイベントや条件によって変化するプロセスを指します。例えば以下サンプルのように注文をコンファームするためには現在の状況がドラフトになっている必要がように、状況やフロー上の妥当性を保証する必要があります。
public class Order {
private OrderStatus status;
public void confirm(InventoryPolicy policy) {
if (status != OrderStatus.DRAFT) throw new DomainException("not draft");
status = OrderStatus.CONFIRMED;
}
}
ドメイン層が理想的に担うべき責任
こちらの内容はChatGPTの答えを簡単に集約した内容です。
- 常に有効な状態を保証(不正な生成や遷移を許さない)。
- 副作用の隔離(時間/乱数/外部I/Oに直接依存せず、抽象Portで表現)。
- ドメイン言語での表現(Value Object/Domain Eventで可読性を高める)。
- 集約境界内の整合性(即時保証)、集約間はイベントで最終的整合性。
上のドメイン言語での表現というのはドメイン層のコードやモデルが 業務や問題領域の言葉(ユビキタス言語, Ubiquitous Language) を直接表現するように設計されていることらしく、ドメイン駆動設計(DDD)の中心的な考え方であり、上記に書いた「ドメインの決定(分割)」にも関係する話かと思います。
🙌 おわりに
自分にとってアーキテクチャは「必ず守るべきルール」ではなく、正しい道へ導いてくれる案内板のような存在です。必ず従う必要はありませんが、その指針を意識することで、無駄を減らし、より少ないコストで目的地へ辿り着けると考えています。
なんてね!!😏
参考
Discussion