この記事は
「ドメイン駆動設計入門」を読んでまとめている第5弾
前回記事はこちら
今回はアプリケーションの組み立てと集約についてまとめる
アプリケーションを組み立てる
ドメインオブジェクト、サービス、リポジトリ、ファクトリなど、アプリケーションを組み立てるために必要な部品の説明が終わった
それらの部品を使ってアプリケーションを組み立てる手順は以下のとおり
- 要求にしたがって必要な機能を洗い出す
- 機能を成り立たせるために必要なユースケースを洗い出す
- 機能を実現するためには単一のユースケースではなく、ユースケースの組み合わせが必要なときもある
- アプリケーションにとって必要な知識・ルールを選び、ドメインオブジェクトをつくる
- ユースケースを実現するために、ドメインオブジェクトを用いてアプリケーションサービスをつくる
サークル機能を実現する
手順 2 に書いたように、ある機能を実現するためのユースケースが1つとは限らない
「サークル機能」を実現するにはいくつかのユースケースが考えられる
さらにサークルには、人数の上限やすでに存在するサークルと同じ名前が使えない、など制約・ルールが存在する
もう少し詳細な手順
機能はトップダウンで洗い出し、実装はボトムアップで組み立てていく
- ルールを持ったドメインオブジェクト(エンティティ、値オブジェクト)をつくる
2. ルールが肥大化する場合は仕様という考え方をつかう(次記事) - 永続化を担うリポジトリをつくる
- オブジェクト生成を担うファクトリをつくる
- ドメインオブジェクトが持つと不自然になるふるまい(例:重複確認)をドメインサービスに切り出す
- ユースケースを組み立てる
- コマンドクラスをつくる
- アプリケーションサービスをつくる
ルールが漏れ出すと・・・
知識やルールがドメインオブジェクトから漏れ出してプロダクトに点在すると
ルール改正のたびに修正漏れのリスクが生じる
対策として集約という考え方がある
集約
データを変更するための単位としてあつかわれるオブジェクトの集まりを集約といいます(P267)
集約には境界とルートが存在する。外部から集約に対する変更はすべてルートを通しておこなわれる。集約の外部から境界内部のオブジェクトを操作してはいけない。
UserName
を操作できるのは、ユーザ集約のルートとなるUser
のみ
user.Name = "Taro" // Bad !! 直接操作してはいけない
user.ChangeName("Taro") // Good!! メソッドをつかえば、NULL検証などルールと照らし合わせてから変更を受け入れることができる
circle.Members.Add(user) // Bad !! Cicle内部のメンバーリストは公開しないほうがいい
circle.Join(user) // Good !! こちらの方が文脈的にも自然
Circle
のMember
リストは外部に公開せず、private
にしておくとよい
デメテルの法則
集約をチェックするための1ツールがデメテルの法則
簡潔に言うと「直接の友達とだけ話すこと」と要約できる (Wikipedia)
Circle
はUser
のことだけ知っている
直接ユーザーの名前プロパティにアクセスするのではなく、「名前を変更してね」とだけお願いするとよい
// 悪い例)ユーザー側でルールを記述している
if(circle.Members.Count > N)
{
// ...
}
// よい例)ルールをサークルクラスに問い合わせている
if(circle.isFull())
{
// ...
}
集約をどう区切るか
User
とCircle
は変更の単位で区切られている
裏を返せばCircle
はUser
を変更してはいけない
もし変更して面倒をみようものなら、サークルリポジトリにユーザ情報変更のロジックが追加(汚染)されてしまう。
当たり前であるが、すでにユーザリポジトリにはユーザ情報変更のロジックがあるため、同じロジックが重複して点在してしまう
ロジックの重複・点在は、後々の修正時に修正漏れを引き起こす・・・
IDによるコンポジション
しかしながら、Circle
はメンバーリストとしてUser
インスタンスを保持しているので、user.ChangeName("Taro")
を呼び出せてしまう
どうすればいいか?
Circle
やUser
はエンティティであるため、IDで識別できる
つまりインスタンス全体を知らなくとも、IDだけでインスタンスを識別できる
IDだけであれば、ほかのメソッドは呼び出されずに済む
class Circle
{
public List<User> Members {get; set;}
// ユーザクラスをつかうと、余計なメソッドを呼ばれる心配がある
// ↓↓↓ 変更 ↓↓↓
public List<UserId> Membsers {get; set;}
// ユーザIDだけつかうことで、情報を隠ぺいできる。むやみにメソッドが呼ばれない
// メモリや再構築の手間も省ける
}
集約の大きさ
集約が変更されるたびにトランザクションがおきる
集約が大きくなると、トランザクションが失敗する可能性も大きくなる
トランザクションが失敗し、リトライ頻度が大きくなるとアプリケーションの質が低下する
大きくなりすぎた集約は境界を見直すべき