はじめに
集約の切り方に正解はなく、経験とともに変わっていくものだと参考にした動画や書籍からも学びました。実装を進めるうちに設計し直しも起きると思いますが、今の自分がどう考えたかを出発点として残しておきたくて書きました。
開発しているシステム
登録した本が図書館の蔵書に追加されたらメールで通知してくれるシステムです。ユーザーが「この本が入ったら教えて」と登録しておくと、バッチ処理が定期的に図書館APIを叩いて蔵書を確認し、追加があればメールを送ります。
主なドメインモデル
-
User:ユーザー -
Book:本 -
Library:図書館 -
UserLibrary:ユーザーが登録したマイ図書館 -
BookSubscription:どの本をどの図書館で監視するかの登録 -
CheckResult:バッチが図書館APIを叩いた結果
設計の手順と集約を決めるタイミング
ドメインモデルとER図がある状態で、集約をどう切るかを考えました。今回の設計手順は以下の通り。
ユースケース図 → シーケンス図 → ドメインモデル → ER図 → 集約の境界を決定 → Repositoryインターフェース
集約はRepositoryをどう分けるかに直結するもので、整合性を保ちつつシンプルに実装できる単位で決めるべきものです。抽象的な概念が固まってからでないと判断できないので、この順番にしました。
なぜテーブル単位のRepositoryではダメなのか?
「テーブルが6つあるから、Repositoryも6つ作るのかな」と最初は思っていました。でもそれは違って、Repositoryはテーブル単位じゃなくて集約単位で作ります。
なぜかというと、テーブル単位にしてしまうと存在しないはずのデータが作れてしまうからです。
今回のシステムで言うとCheckResultがわかりやすい例で、CheckResultってBookSubscriptionがないとそもそも存在しないんです。監視登録がなければチェックも走らないので、必ずBookSubscriptionのIDを持ちます。
それを別々のRepositoryにしてしまうと、CheckResultだけ単独で生成できる状態になってしまう。集約単位にすることで、「こいつはこいつがいないと存在できない」という関係をコードで表現できます。
集約の境界を引く3つの判断ポイント
ポイント1:ライフサイクルが一致するか?
そのエンティティが、別のエンティティなしに存在できるかどうかです。
CheckResultはBookSubscriptionがなければそもそも存在しません。バッチ処理の中でしか生まれない。親なしに子は存在できない。
新着通知システムの場合:CheckResult → YES(一致する)→ BookSubscriptionと同じ集約
ポイント2:独立した操作があるか?
そのエンティティ単体を対象としたユースケースが存在するかどうかです。登録だけじゃなく、単体で削除・更新されるケースがあるかも含みます。
UserLibraryは「図書館を登録する」「図書館を削除する」というユースケースが独立して存在します。BookSubscriptionとは無関係に操作できます。
新着通知システムの場合:UserLibrary → YES(独立した操作がある)→ 別集約
ポイント1と2は裏表
この2つは同じことを違う角度から確認しています。独立した操作がある = ライフサイクルが一致しない。どちらかで答えが出たらもう片方も同じ結論になります。
もし両方YESになったら、集約ルートの設定を間違えている可能性があります。
ただし、複数の集約から参照されているエンティティは、ライフサイクルが一致しているように見えても別集約にすべきケースがあります。それはポイント3で確認します。
ポイント3(補助):参照か、所有か?
ポイント1・2で迷ったときの補助確認です。IDを持っているだけで「同じ集約では?」と混乱するケースに使います。
BookSubscriptionはuser_idとbook_idを持っています。「UserとBookに依存してるから同じ集約じゃないの?」と思いました。
でもUserもBookもBookSubscriptionなしに存在できます。BookSubscriptionはIDで指さしてるだけで、ライフサイクルを管理していません。これが「参照」。所有じゃない。
新着通知システムの場合:User・Book → 参照されてるだけ → 別集約
結論:新着通知システムの集約構成
Repositoryはテーブル数の6ではなく、集約単位の4になりました。
おまけ:DTOはどこで組み立てるか?
バッチの最後にメール送信用に本のタイトル・メールアドレス・チェック結果をまとめる必要がありました。これは複数の集約をまたぐ参照処理です。
置き場所の判断はこうです。
- Repository:集約の生成・取得・保存だけ
- ApplicationService:複数集約をまたぐデータの組み立て
- ドメインサービス:複数集約にまたがるビジネスルールの判断が必要なとき
今回は「データをまとめてメール送信に渡す」だけで、ビジネスルールの判断はありません。ApplicationServiceで正しい。
@dataclass
class CheckNotification:
book_title: str
user_email: str
status: str
reserve_url: str
参考
(敬称略)
-
『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』成瀬 允宣