はじめに
本記事では、ドメイン駆動設計のパターンとして用いられるドメインサービスについて、初心者向けに解説したものです
対象読者
- ドメイン駆動設計における ドメインサービスが何かがわからない方
前提条件
ドメイン駆動設計で登場する値オブジェクト、エンティティの概念について理解していること
ドメイン駆動設計におけるサービスとは、
大まかに、
- アプリケーションサービス
- ドメインサービス
がありますが、本記事ではドメインサービスについて解説しています。
結論
- ドメインサービスとは、値オブジェクト・エンティティで表現するには不自然 or 複雑な業務ルールを表現したもの
- 値オブジェクトやエンティティの責務が増えすぎるのを防ぐ役割がある
- ドメインサービスを定義する際は、ドメインサービスの責務を明確にし、それに関するルール(メソッド)のみ定義すること
複雑な業務ルールを表現するのがドメインサービス
ドメインサービスは、値オブジェクトやエンティティが表現するには不自然または、表現できない複雑な業務ルールを表現するために用いられます。
値オブジェクトやエンティティで表現できないルールをドメインサービスへ定義することで、
各ドメインオブジェクト同士の責務がシンプルになるメリットがあります。
では、値オブジェクトやエンティティが表現するには不自然・複雑なルールの例を見てみましょう。
ユーザーIDの重複確認は誰がやる?値オブジェクト・エンティティで表現するには不自然なルール
SNSでは、利用者が自由にユーザーIDを変更できるのが一般的です。
しかし、条件として他の利用者と同じユーザーIDを設定することはできないケースが多いです。
この業務ルールを表現するために、ユーザー情報を表現するエンティティでは、同じユーザーIDを設定しているアカウントが存在するか確認するメソッドを用意しました。
import java.util.UUID;
// UserInfoエンティティクラス
public class UserInfo {
private final UUID uniqueId; // ユーザーを識別するユニークID(変更不可)
private String userId; // ユーザーID
private String password; // パスワード
// コンストラクタ
public UserInfo(String userId, String password) {
this.uniqueId = UUID.randomUUID(); // 自動生成
this.userId = userId;
this.password = password;
}
// Getter: ユニークIDは変更不可
public UUID getUniqueId() {
return uniqueId;
}
// Getter & Setter: 他のプロパティは変更可能
public String getUserId() {
return userId;
}
public void changeUserId(String userId) {
this.userId = userId;
}
// ユーザー名が存在するかを確認するメソッド
public boolean isExist(UserInfo user) {
// ユーザーIDの重複確認処理(詳細は省略)
}
// 以降のセッター、ゲッターメソッドは省略
}
重複するユーザーIDが存在するか確認するには、以下のように行います。
public class Main {
public static void main(String[] args) {
// ユーザーを作成
UserInfo suzuki = new UserInfo("suzuki", "password123");
UserInfo tanaka = new UserInfo("tanaka", "password456");
// tanaka というユーザーIDが存在するか確認する処理
boolean isExist = suzuki.isExist(tanaka);
}
}
ここで以下のコードに注目してください。
tanaka というユーザーIDが存在するかの確認を、suzukiというユーザーに対して問い合わせしています。
// tanaka というユーザーIDが存在するか確認する処理
boolean isExist = suzuki.isExist(tanaka);
このコードは違和感があります。
なぜなら、重複するユーザーIDの確認は、一ユーザーが担う責務ではないからです。
ユーザーIDの重複確認は、別のドメインオブジェクトで行うのが自然です。
この違和感を解決するのが、ドメインサービスです。
ドメインサービスを導入して不自然さを取り払ったコード
ユーザーIDの重複確認をユーザー個人が行うのは不自然だとわかりました。
値オブジェクトやエンティティが表現するには不自然または、表現できない複雑な業務ルールは、ドメインサービスクラスへ抽出して解決します。
public class UserDomainService {
// コンストラクタ
public UserDomainService() {
}
// ユーザーIDが存在するかを確認するメソッド(UserInfoクラスから抽出)
public boolean isExist(UserInfo user) {
// ユーザーIDの重複確認処理(詳細は省略)
}
}
public class Main {
public static void main(String[] args) {
// ユーザーを作成
UserInfo suzuki = new UserInfo("suzuki", "password123");
UserInfo tanaka = new UserInfo("tanaka", "password456");
// tanaka というユーザーIDが存在するか確認する処理
UserDomainService domainService = new UserDomainService();
boolean isExistTanaka = domainService.isExist(tanaka);
// suzuki というユーザーIDが存在するか確認する処理
boolean isExistSuzuki = domainService.isExist(suzuki);
}
}
不自然な処理が取り払われたため、UserInfoはシンプルなクラスになりました。
import java.util.UUID;
// UserInfoエンティティクラス
public class UserInfo {
private final UUID uniqueId; // ユーザーを識別するユニークID(変更不可)
private String userId; // ユーザーID
private String password; // パスワード
// コンストラクタ
public UserInfo(String userId, String password) {
this.uniqueId = UUID.randomUUID(); // 自動生成
this.userId = userId;
this.password = password;
}
// Getter: ユニークIDは変更不可
public UUID getUniqueId() {
return uniqueId;
}
// Getter & Setter: 他のプロパティは変更可能
public String getUserId() {
return userId;
}
public void changeUserId(String userId) {
this.userId = userId;
}
// 以降のセッター、ゲッターメソッドは省略
}
複雑な業務ルールも表現できるドメインサービス
今度はユーザーアカウントの作成業務についても考えてみましょう。
ユーザーアカウントの登録は、以下の流れで行うと仮定します。
- 利用者は、登録するユーザーIDとパスワードを入力する
- システムは、入力されたユーザーIDが存在するか確認する
- 同じユーザーIDが既に登録済みの場合は、エラーとする
- ユーザIDが重複していない場合は、登録処理を行う
ユーザーIDが重複しているか確認 → 問題なければ登録を行う。
この流れをユーザーが行うのは、不自然かつ複雑になります。
この業務ルールもドメインサービスへ抽出します。
複雑な業務ルールを定義したドメインサービスのコード
public class UserDomainService {
// コンストラクタ
public UserDomainService() {
}
// 新しいユーザーを登録する
public void create(UserInfo targetUser) {
if(isExist(targetUser)) {
throw new Exception("同じユーザーIDが登録済みです")
}
// ユーザーの登録処理(詳細は省略)
}
// ユーザー名が存在するかを確認する
public boolean isExist(UserInfo user) {
// ユーザーIDの重複確認処理(詳細は省略)
}
}
public class Main {
public static void main(String[] args) {
// 登録するユーザーIDとパスワードを入力
UserInfo tanaka = new UserInfo("tanaka", "password456");
try {
// tanaka というユーザーを登録する
UserDomainService domainService = new UserDomainService();
domainService.create(tanaka);
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println("登録成功");
}
}
ドメインサービスの多用には要注意
ドメインサービスを定義するときは、役割を1つに絞ることが大切です。
ドメインサービスはどんな処理でも書けてしまうため、意識せずに設計すると多重責務を背負ったクラスになりやすいです。
ドメインサービスへ過度に処理を書いてしまうと、値オブジェクト・エンティティはただのデータをいれるだけのクラスになってしまいます。
値オブジェクト・エンティティが本来持つべきルールや振る舞いが失われたドメインオブジェクトの状態を、ドメインモデル貧血症と呼びます。
値オブジェクト・エンティティ、ドメインサービスのどちらにルール(メソッド)を定義するか迷ったら、一旦値オブジェクト・エンティティへ定義するようにしましょう。
不自然に感じても、関連する処理は一箇所に集めることを優先しましょう。
さいごに
値オブジェクトやエンティティに定義するには不自然・複雑な業務ルールは必ず存在します。
これは複数のドメインオブジェクトを横断するような操作によく見られます。
そんな時に役に立つのが、ドメインサービスです。
ドメインサービスは便利ですが、どんな処理も書けてしまうためドメインモデル貧血症を招く恐れがあります。
そのふるまいはどこに定義するべきなのか、誰がの責務なのか を意識するようにしましょう。
ここまで読んでいただきありがとうございました!