読書メモ:ドメイン駆動設計 サンプルコード & FAQ
September 4, 2022
以下の本を読んだ。あとで見返したいところについてメモを残しておく。
- ドメイン駆動設計 サンプルコード & FAQ
- https://little-hands.booth.pm/items/3363104
*記事中のコードの言語は Kotlin です。
第 3 章:エンティティ/値オブジェクト #
プライマリコンストラクタが1つしか実装できない場合 - P.39 #
再構成メソッドは、プロダクトコードではリポジトリ実装クラスで DB の値からインスタンス再構成する場合のみ使用します。
一方、テストコードの中では、テストに必要な任意のインスタンスを作成するために使用できます。そのため、fromRepository といった名前ではなく reconstruct という名称にしていますが、名前は任意のものをつけて構いません。
コンストラクタとファクトリーメソッドの違い - P.39 #
質問 #
コンストラクタと、static なファクトリーメソッドは何が違うのでしょうか?
回答 #
名前がつけられるということです。それ以外に大きな違いはありません。
通常のインスタンス化と再構成によるインスタンス化の2つを持つ場合は、同列にファクトリーメソッドとして揃えた方がわかりやすければどちらもファクトリーメソッドとしてください。
通常のインスタンス化だけであれば名前のないコンストラクタで実装しても問題ありません。
ただし、再構成メソッドに関しては、名前をつけて専用のメソッドであることを示すのがよいでしょう。
OR マッパーが自動生成するカラムの扱い - P.41 #
質問 #
OR マッパーが自動で生成する created_at(作成日時)や updated_at(更新日時)について、画面表示に使っても良いでしょうか?
回答 #
画面表示に使用するのであればエンティティ/値オブジェクトに「作成日時」などの属性を定義し、値の設定はクラス内のメソッドで制御しましょう。理由は2つあります。
1つは、値を設定するタイミングの問題です。OR マッパーはリポジトリの実装クラス内でのみ使用するので、エンティティ/値オブジェクトのインスタンスを生成してからリポジトリに渡すまでは OR マッパー側で設定できません。そのため、リポジトリに渡すまで一時的に作成日時/更新日時が null 状態のインスタンスが存在することになりますが、これは「常に正しいインスタンスしか存在させない」という方針に反します。
もう1つは、値の設定内容の問題です。特に更新日時は、OR マッパーが設定する日時とドメイン層のロジックで意図する日時が異なることがあります。たとえば、OR マッパーは更新処理が走ったら毎回更新する一方、ドメイン層のロジックとしては特定のカラムの更新は更新日判定に含めない、といったことが考えられます。ここをドメイン層のロジックにしたがって制御したい場合は、ドメイン層に自分で実装する必要があります。
処理に使用しない値も取得する必要はある? - P.44 #
質問 #
エンティティに5つの属性があった時、あるエンドポイントの処理ではそのうち2つしか必要になりません。残りの3つを DB から取得するのは無駄ではないでしょうか。
回答 #
DDD の実装パターンでは、DB アクセスの効率性を多少犠牲にし、保守性を高めることを目指します。実装パターンの採用に関しては、このトレードオフを考慮して判断する必要があります。
具体例で説明します。オブジェクトに対応するテーブルで一部カラムだけ更新する場合は、SQL 効率だけを考えれば UPDATE 文1回で実行できます。しかし、SQL 実行にあたって分岐やバリデーションが入ってくると、ドメイン知識(ルール/制約)がドメイン層とインフラ層に散在して結合度が高まる、テスト時にインフラ層をモックしにくくなるといった問題が発生します。
それを避けるために、エンティティ/値オブジェクトのメソッドでドメイン知識に関する実装を完結し、インフラ層ではシンプルな DB との入出力だけに留めます。そうすることで、高凝集/低結合となり、可読性やテスト容易性が向上します。そして、これを実現するために「一度リポジトリでエンティティ/値オブジェクトの値を全て取得してインスタンスを再構成し、更新されたインスタンスをリポジトリ経由で DB に反映する」という手法をとります。
ここで大切なことは「DB アクセス効率が下がる」というコストと「可読性やテスト容易性を上げる」というリターンを考えた時に、リターンの方が大きいと考えられるかです。そのように考えられる場合はこの方法を採用し、そうでない場合は目的にあった実装を検討しましょう。
なお「DB アクセス効率が下がる」ことが参照系処理で許容されない場合があります。その場合はドメイン(更新用)モデルとは別にクエリ(参照用)のモデルを使用するという手段があります(CQRS)。
DB のオートインクリメントの値を ID に使用してよい? - P.45 #
質問 #
エンティティの識別子に、DB のオートインクリメントの値を使用することは可能でしょうか。
回答 #
可能ですが、考慮事項がいくつかあります。
まず、実装方法を整理します。オートインクリメントの値を識別子として扱う方針は次の2通りが考えられます。
- 方針1:リポジトリ内でエンティティを永続化する際に初めて採番する
- 方針2:エンティティ生成前に DB で採番した値を取得し、エンティティに渡す
方針1は、Active Record 型の OR マッパーを使った時の挙動と近いため、そのような OR マッパーに慣れている人には馴染みやすい実装になります。しかし、リポジトリに渡す前に一時的に ID が存在しないインスタンスができてしまうため、「常に正しいインスタンスしか存在させない」という方針に違反します。
DB のオートインクリメントの値を使用するのであれば方針2の実装をおすすめします。
なお、ID の事前採番に関しては、DB のオートインクリメント以外に ULID や UUID などのアルゴリズムの仕様を検討してみてください。プログラムコードのみで採番できるため、ID 採番が DB に依存しなくなり、保守性が向上します。
第 4 章:リポジトリ #
リポジトリに件数チェックメソッドを書いてもよい? - P.48 #
質問 #
リポジトリに件数を取得するメソッド、存在をチェックするメソッドを定義してもよいでしょうか?
回答 #
問題ありません。リポジトリはエンティティ/値オブジェクトの集合を表すオブジェクトであり、件数や存在チェックなど集合に関する情報を返すメソッドは責務として矛盾がないと考えられます。
存在チェックはエンティティを返すメソッドや件数取得メソッドでも代用できますが、Boolean を返すメソッドを別途用意してもよいでしょう。
リポジトリでソート順を指定してもよい? - P.49 #
質問 #
リポジトリを通じた値の取得結果をソートするにはどうしたら良いでしょうか?
回答 #
リポジトリの引数に「並び替えに使用する条件」を表す enum を渡すのがよいでしょう。
package dddfaq.domain.user
enum class UserOrderKey{
NAME,
USER_ID
}
interface UserRepository{
fun findByStatus(status: UserStatus, userOrderKey: UserOrderKey): List<User>
}
ここで、OrderKey はドメイン層が持つ知識として問題ないのか、という疑問が生じます。
OrderKey が示すものが RDB のテーブル定義されているカラム名だとすると、ドメイン層にインフラ層の知識を持つことになるので問題があります。しかし、「ユーザーエンティティの名前で並び替えて」「ID で並び替えて」というように、ドメイン層にある情報のみで表現されていれば問題ありません。「ソートをどのように実現するか(SQL であれば ORDER BY 句を使用する)」がドメイン層に漏れ出ていなければよいのです。
OrderKey を受け取ったリポジトリがソートを実現するには、次のように実装します。OR マッパーには jOOQ(Java 製の OR マッパー)を使用しています。
package dddfaq.infra.repository
class UserJooqRepository{
override fun findByStatus(
status: UserStatus,
userOrderKey: UserOrderKey
): List<User>{
val orderColumn = buildOrderColumn(userOrderKey) // * 1
return jooq.selectFrom(USERS)
.where(USERS.STATUS.eq(status.name))
.orderBy(orderColumn) // * 2
.fetch()
.map { userRecord ->
// Record からエンティティに詰め替え
User.reconstruct {
id = UserId(userRecord.userId),
name = userRecord.name
}
}
}
private fun buildOrderColumn(userOrderKey: UserOrderKey): SortField<String>{
return when (userOrderKey){ // * 3
UserOrderKey.USER_ID -> USERS.USER_ID.asc()
UserOrderKey.ROLE -> USERS.ROLE.asc()
}
}
}
*1 で buildOrderColumn メソッドを使用して UserOrderKey を *2 orderBy に渡すカラム情報に変換することにより、指定した順序でのソートを実現します。
*3 は Kotlin の文法で、when に渡す値で分岐した処理を記述できます。他の言語では Switch というキーワードで定義されていることがあります。
複数エンティティを一括雨更新してもよい? - P.52 #
質問 #
リポジトリに、複数のエンティティをまとめて更新するメソッドを設けてもよいでしょうか?
回答 #
問題ありません。次のように、単一エンティティを処理するメソッドとは別に、複数エンティティをまとめて更新するメソッドを設けることができます。
interface UserRepository{
fun insert(user: User) // * 1 単一エンティティのみインサート
fun insert(users: Set<User>) // * 2 複数エンティティ同時にインサート
}
こうすることにより、*2 の実装クラスのメソッドの中で一括処理パフォーマンスを最適化できます(RDB であれば、バッチインサートを行うなど)。
*1 のメソッドをユースケースから複数回呼ぶ実装でパフォーマンスに問題がある場合は、*2 のメソッドの作成を検討します。実装の複雑度が少し上がり、必要なテストが増えるため、パフォーマンスに問題がなければ必ずしも作成する必要はありません。
insert と update のメソッドを一緒にしてもよい? - P.53 #
質問 #
insert と update の両方を行える、save や upsert といったメソッドを作成してもよいでしょうか。
回答 #
問題ありません。ただし、メリットデメリットを考慮して使い所は検討しましょう。
メリットとしては、呼び元のユースケースで新規保存か更新かを意識しないで使えるようになることです。一方、デメリットとしてはリポジトリのメソッドの中で「insert か update か」を判断し処理を分岐するため、実装が複雑になることです。凝集度の観点からすると、責務が2つになり低凝集になると説明できます。
ユースケースの段階で新規保存か更新が分かれていることは多く、そのような場合はデメリットがメリットを上回ります。本当に1つのメソッドにする必要があるかは検討し、必要なところだけ統合するのがよいでしょう。
集約の子オブジェクトをどうやって更新する? - P.54 #
質問 #
集約ルートが子オブジェクトを複数持っている場合、リポジトリの update メソッドで子オブジェクトをどのように更新すればよいでしょうか?
回答 #
たとえば、User エンティティが集約ルートで、集約内の子オブジェクトとして追加/削除可能な複数の MailAddress を持っているような場合ですね。次のようなコードで、users テーブルと、email_addresses テーブルに永続化しているとします。
data class MailAddress(val value :String)
class User(
val name: String,
val mailAddress: Set<MailAddress>
) {
// メールアドレスの追加メソッド、削除メソッドを持つものとする
}
interface UserRepository{
fun insert(user: User)
fun update(user: User) // * 1
}
User エンティティが集約ルートなので、*1 update メソッドには複数の MailAddress を子オブジェクトとして保持した User インスタンスを渡します。ここで、子オブジェクトの MailAddress は新規保存するのか、更新するのか、削除するのか、どのように判断すればよいでしょうか。
一番簡単な方法は、一度該当 User に紐づく email_addresses テーブルのデータを削除して、User エンティティが保持していた mailAddresses を全てインサートする実装です。シンプルで、バグを生み出しにくいというメリットがありますが、他のテーブルから外部キー参照されていたり、created_at といった自動設定カラムの値を保持する必要がある場合はこの手段が取れません。
もう1つの方法は、リポジトリの中で email_addresses テーブルのデータを取得し、リポジトリに渡された mailAddresses と比較して「新規保存する値・変更する値・削除する値」を分岐する方法です。実装が複雑になりますが、1つ目の方法が取れない場合は避けられません。テストをしっかり書いて動作を保証しましょう。
ファイルや外部サービスにデータを保存する処理 - P.55 #
質問 #
ファイルを保存したり、外部サービスにデータを登録するようなオブジェクトも、リポジトリという名称で揃えたほうが良いでしょうか?
回答 #
性質が異なるものには、個別の名前をつけた方がよいでしょう。
例として、ファイル保存するオブジェクトは XxxStorage、外部サービスにリクエストするオブジェクトは XxxClient といった名称が考えられます。
第 5 章:集約 #
複数集約の整合性を確保する方法 - P.57 #
集約を実装する上で難しいのは、複数集約にまたがった整合性を確保する方法です。
複数集約間の整合性確保が必要な例として、次のようなものを考えます。
とあるタスク管理アプリケーションで、タスクを作成したら「{タスク名}が作成されました」という活動履歴を作成することになりました。履歴に漏れがあると困るため、タスク作成時には確実に活動履歴が作成されるようにしたいです。つまり、タスク集約と活動履歴集約、という2つの集約間に確保したい整合性があるということです。
実装方法は、主に次の3つに分かれます。
- ユースケースで複数集約に更新をかける。
- ドメインサービスを使用する。
- ドメインイベントを使用する。
実装方法 1. ユースケースで複数集約を更新する #
一番シンプルな実装は、ユースケースで複数集約のインスタンスを生成し、それぞれをリポジトリに渡す方法です。まずはドメイン層のコードは次のようになります。
package dddfaq.domain.task
/** タスク */
class Task(
val taskName: String
)
interface TaskRepository{
fun insert(task: Task)
}
package dddfaq.domain.activityhistory
/** 活動履歴 */
class ActivityHistory private constructor(val detail: String) {
companion object {
fun createFromTask(task: Task): ActivityHistory {
return ActivityHistory("${task.taskName}が作成されました"))
}
}
}
interface ActivityHistoryRepository{
fun insert(activityHistory: ActivityHistory)
}
このクラスを使用するユースケースは次のようになります。
package dddfaq.usecase.task
class CreateTaskUseCase(
private val taskRepository: TaskRepository,
private val activityHistoryRepository: ActivityHistoryRepository,
) {
@Transactional
fun execute(taskName: String) {
// Taskの作成と保存
val task = Task(taskName)
taskRepository.insert(task)
// 生成したTaskを使用してActivityHistoryを作成し、保存
val activityHistory = ActivityHistory.createFromTask(task)
activityHistoryRepository.insert(activityHistory)
}
}
ユースケースの記述で2つの集約の整合性が確保されています。シンプルに実装できるというメリットがあるため、DDD 導入初期にはまずこの方法が採用されることが多いです。
しかし、デメリットがあります。別のユースケースでうっかりと集約間の整合性を破壊してしまうことが可能だということです。
また、「タスクが作成されたら活動履歴も作成される」ということはドメイン層の知識として重要なのに、その知識がドメイン層に書かれておらず、ドメイン層のコードを読んでも認識できないという問題もあります。
実装方法 2. ドメインサービスを使用する #
実装方法 1 のデメリットに対する改善案が、ドメインサービスを使用する方法です。
まず、ユースケースは次のように変わります。
package dddfaq.usecase.task
class CreateTaskUseCase(
private val taskCreator: TaskCreator
) {
@Transactional
fun execute(taskName: String) {
taskCreator.create(taskName)
}
}
TaskCreator はドメインサービスです。
ユースケースからはドメインサービスを呼ぶだけになり、2つの集約間の整合性を確保する責務はそちらに移譲します。
package dddfaq.domain.task
class TaskCreator(
private val taskRepository: TaskRepository,
private val activityHistoryRepository: ActivityHistoryRepository,
) {
fun create(taskName: String) {
val task = Task(taskName)
taskRepository.insert(task)
val activityHistory = ActivityHistory.createFromTask(task)
activityHistoryRepository.insert(activityHistory)
}
}
ひとまず集約間の整合性の知識をドメイン層に移譲できました。しかし、まだ問題があります。これでも他のユースケースで整合性を破壊することは防げないのです。
これを防ぐために1つの工夫をします。
なお、この実装は Kotlin の言語使用に依存した実装になっているため、異なる言語では必要に応じてカスタマイズしてください。
(書籍に記載のコードは省略)
Kotlin の文法では、インターフェイスの可視性を sealed にすると、同じパッケージ内でしか実装クラスを作成できなくなります。またクラスの可視性を private にすると、そのクラスを同じファイル内でしか参照できなくなります。これを組み合わせることにより、「絶対にタスクと活動履歴がセットで作られる」という制約をかけることができます。
これにより、実装方法 1 のデメリット「集約間の整合性の知識がユースケースに漏れ出ている」「うっかり集約間の整合性を破壊できる」という点を克服できました。
実装方法 2 のデメリットは、ドメイン層の「サービス」の濫用を招き、ファットなドメインサービス、ドメインモデル貧血症(ドメインロジックを持たない)なエンティティ/値オブジェクトを誘発する可能性があることです。
実装方法 3. ドメインイベントを使用する #
3つ目の方法では、ドメインイベントを使用します。
全体の流れは次の3ステップになります。
- エンティティの特定の処理のタイミングでドメインイベントを作成し、エンティティ内に蓄積する。
- リポジトリの insert/update が成功後、イベントを発行(publish)する
- 発行されたイベントをイベントリスナーの仕組みで拾い、他集約の処理を実施する
3 のイベント発行とそれを拾う仕組みはフレームワークに依存した実装になります。本書では Java のフレームワーク Spring の仕組みを使って実装したサンプルを示しますが、他のフレームワークに適用する際には適宜カスタマイズしてください。
(以降の記載は省略)
どの実装方法を選択すればよいか #
複数集約間の整合性を確保する実装方法を3つ紹介しました。
この3つはどれが正解ということはなく、実装コストと整合性を守る重要性のバランスを考えてどれを採用するか決定します。バランスを考慮した結果、1つのプロジェクトの中で複数の実装方法が混在しても問題ありません。
複数集約を 1 トランザクションで更新してよい? - P.72 #
質問 #
DDD では 1 トランザクションで 1 集約しか更新してはいけないという記述をみたことがあります。実際には 1 つの処理の中で複数集約を更新したいこともあるのですが、これはよくないことでしょうか?
回答 #
1 トランザクションにおける複数集約更新に関しては賛否両論がありますが、メリットデメリットを考慮した上で判断できれば問題ないと考えています。筆者は普段は許可する方針で開発しており、十分に実践的です。
1 トランザクションで複数集約更新するデメリットは「トランザクションをかける範囲が大きくなり、ロック取得や排他エラーにする範囲が大きくなりすぎる場合がある」ことです。一方メリットは途中で例外が発生した場合ロールバックができることです。この 2 つを考慮した結果、メリットの方が大きいと判断すれば、1 トランザクションでの複数集約を許容しています。
第 6 章:ドメインサービス #
ドメインサービスでリポジトリを使用してもよい? - P.76 #
質問 #
ドメインサービスがリポジトリを使用するのは良くないでしょうか?
回答 #
賛否両論ありますが、問題ないと考えています。
ドメインサービスの定義から検討します。ドメインサービスは「エンティティ、値オブジェクトとしてモデリングすると不自然なドメイン知識(ルール/制約)を実現する物」という定義です。その代表例が、集合に対する操作です。
たとえば「予約」というエンティティについて、「この時間に予約が既に埋まっているか」というのは予約エンティティ自体が知っているとするのは不自然です。そこで、予約有無に関するドメイン知識を実現するものとしてドメインサービスを定義し、予約の集合に対する操作を行うためにリポジトリを使用することになります。このような用途を考えるとドメインサービスがリポジトリを使用することは必要になると考えます。
第 7 章:テスト #
値オブジェクトが独立することによるテスト容易性の向上 - P.87 #
Task クラスには taskName 以外にも postponeCount、dueDate などテスト対象となる属性が複数存在するため、それら全てのテストを Task クラスのテストに実装しようとすると Task クラスに対するテストコード量がどんどん増大していきます。
そこで TaskName クラスを独立させることにより、タスク名の文字数に関するテストを TaskName クラスのテストに書けるようになりました。この際、テスト観点に関係ない他の属性を記述する必要がないため、テストコードの記述を非常に簡潔にできました。
TaskName クラスは「タスク名を表すクラス」と責務が明確で高凝集/低結合になっています。今回の事例のように、高凝集/低結合な実装にするとテスト容易性、可読性が高まります。
第 8 章:アーキテクチャ #
ユーザー表示するエラーメッセージを直接返してよい? - P.105 #
質問 #
API サーバーでエラーが発生した場合のメッセージについて、サーバーからエンドユーザーに表示するメッセージを直接返してよいでしょうか。それとも、エラーコードのみ返してメッセージはフロントで定義するべきでしょうか。
回答 #
どちらでも可能です。メリットデメリットがあるのでそれを考慮して決定します。
サーバーサイドから表示用メッセージを直接返す方針のメリットは、実装がシンプルになり、工数が全体的に低くなる点です。デメリットは、メッセージ変更の際にサーバーサイドのコードを変更する必要があるため、フロントエンドの実装だけでメッセージを含めた見た目の開発を完結させられなくなる点です。
これをどう評価するかは、チームの体制によって変わります。サーバーとフロントの開発チームがきっちり分かれて極力個別で開発を進めたい場合はエラーコードのみを返す方針にし、ある程度両方修正できるメンバーが存在し柔軟に対応できる場合はメッセージを直接返す方針にする、と判断してもよいでしょう。
外部システムのクライアントクラスはどの層に書くべき? - P.108 #
質問 #
外部システムと連携する場合、その処理はどの層に実装すればよいでしょうか。
回答 #
外部システムのクライアントオブジェクトのインターフェイスはドメイン層、実装クラスはインフラ層に定義します。リクエスト/レスポンスの型はドメイン層です。
たとえば、Slack 通知をする実装を考えてみます。ドメイン層には、次のオブジェクトを定義します。
data class Notification(
val targetUserId: userId,
val message: String,
)
interface NotificationClient {
fun notify(notification: Notification)
}
この際、ドメイン層の知識として「通知をどう実現するか」に関心がない場合は、その方法(Slack)の知識はぼかして、「通知」という抽象的なクラスとして実現します。
そして、インフラ層に次のクラスを実装します。
class SlackNotificationClient: NotificationClient {
override fun notify(notification: Notification) {
// Slack で実際に通知を送信する実装
}
}
このクラス内で Slack の API にリクエストを送信しますが、その実装の詳細(どのようなクライアントライブラリを使用するのか、など)はインフラ層に閉じ込めます。それによりドメイン層は純粋に「通知を送信する」ということだけを意識すればよく、詳細な実装方法の影響をうけなくなります。
なお、通知方法が Slack であることがドメインの知識として重要な場合、ドメイン層のオブジェクトに Slack という名称を出しても問題ありません。たとえば、通知の種類が Slack とメールの2つあり、それぞれのパターンを意識的に使い分けたい場合などです。
その場合も、通知を実行するための詳細はインフラ層に閉じ込めることは同様です。
ユースケースの戻り値クラスはどこに定義すればよい? - P.118 #
質問 #
ユースケースの戻り値を DTO というクラスで定義しました。そのクラスの置き場所はどこにすべきでしょうか?
回答 #
ユースケースクラスと同じパッケージや同じファイルに定義しましょう。DTO クラスは、「ユースケースの戻り値」を表すのが責務なので、そのユースケースの近くに定義するべきです。
非推奨なのは「dto」といったパッケージをくくり出してしまうことです。
このような配置にすると、DTO とユースケースの関連が追いにくくなるというデメリットが生まれるのに対して、あまり大きなメリットはありません。
ユースケースの戻り値クラスに書式変換メソッドを実装してよい? - P.119 #
質問 #
ユースケースの戻り値用のクラス(DTO)に表示に関するメソッド(数値の書式変換など)を実装するのはありでしょうか?
回答 #
いいえ、責務違反のため、非推奨です。ユースケースの戻り値用クラスはユースケース層のクラスで、表示に関する処理はプレゼンテーションの責務です。表示用の変換処理は、プレゼンテーション層のクラスで受け取った DTO に対して変換処理をする形で実装します。ユースケースのクラスは、プレゼンテーション層が何であろうと影響がない実装にした方が保守性が高まります。
ライブラリに依存してよいのはどの層まで? - P.121 #
質問 #
オニオンアーキテクチャにおいて、特定のライブラリやフレームワークに依存してよいのはどの層まででしょうか?
回答 #
ドメイン層とユースケース層では、極力特定のライブラリ、フレームワークに依存しないようにしましょう。何かライブラリを使った処理を行いたい場合は、ドメイン層とユースケース層ではインターフェイスのみ定義し、インフラ層に定義する実装クラスの中でライブラリを使用します。
トランザクションはどう扱えばよい? - P.122 #
質問 #
オニオンアーキテクチャでトランザクション処理はどのように管理すればよいでしょうか?
回答 #
ユースケースクラスのメソッド入り口でトランザクションを開始し、メソッドが正常終了したらコミット、例外が発生したらロールバック、とします。
ユースケースからユースケースを呼んでよい? - P.122 #
質問 #
ユースケースクラスから別のユースケースクラスを呼んでもよいでしょうか。
回答 #
推奨しません。ユースケース同士の参照を許してしまうと、複雑な処理では1つのユースケースから別のユースケースを呼び出していく階層が深まり、コードを読む際に全体像を追うのが難しくなるためです。
そのため、
- コントローラーから呼ばれるユースケースクラスは必ず1つのみ
- 複数ユースケースクラスから呼ばれる処理はユースケース層に独立クラスを定義して、ユースケースクラスから参照させる
という方針をおすすめします。
複数集約で使用されるオブジェクトはどこに定義する? - P.124 #
質問 #
ドメイン層で複数の集約にまたがって参照されるオブジェクトは、どのようなパッケージに配置すればよいでしょうか?
回答 #
2つの方法があります。
- そのオブジェクトを定義するパッケージに配置する
- 共有パッケージに配置する
1 の例として、UserStatus というオブジェクトを User エンティティがプロパティとして保持し、Task エンティティではある処理の分岐で使用しているとします。これは User 集約の中で UserStatus を定義し、Task 集約からはそれを参照しているという関係性になります。その場合は、User と UserStatus を user パッケージに配置し、どこで定義されているかをパッケージで示します。
2 は、特定の集約で定義されているわけではない抽象的なもの、たとえば日付を表すクラスや ID の基底クラスなどは、「shared」といった共有パッケージを定義しそこに配置します。
複数集約から参照するからと言ってなんでも共有パッケージに配置してしまうと、「定義する側」「参照する側」という関係が見えなくなってしまいます。きちんとどこで定義されているかを意識してパッケージ配置することが重要です。