ドメインモデリング再考
Aggregate Root の目的は、invariants を保護すること。
Aggregate の境界内でビジネスルールを強制すること。
それを保証するのに必要なだけのコードを含んでいれば良い。
設計の方針を立てるのに DDD の考え方を採用しようと思った時に、集約ルートと永続化の関係で考えが詰まってしまった。例えば、ユーザと記事が1:多の関係であり、ユーザ一覧ページと記事一覧ページがあったとする。よくある階層構造のページ遷移が想像できる。 ここで、例えばリポジトリはどういった単位で作るべきなのだろうか。ユーザ用の
例えば、集約ルートとして車を考える。車はいくつもの複雑なコンポーネントから構成されているため、集約ルートは非常に大規模なものになってしまうことが予想されるし、そのようなモデルを扱うのはパフォーマンス的に問題があるのではないか?という懸念も生まれる。
集約ルートとは、集約ないのコアとなる Entity であり、集約ないの全ての要素間でビジネスルールを保つ責務を持っている。ユニークなIDをもつ。
ドメインモデリングとは
モデリング、といった時に、アプリケーションで扱うデータの名前や型を定義して満足してしまうことがある。例えば、以下のような形で。
code:swift
struct User {
let name: String
let age: Int
let sex: String
}
DB を利用した簡単なアプリケーションを作った経験があり、ドメインモデリングの経験がなかった場合、データモデリングの視点が抜けきれず、このようなモデリングで満足してしまう。これは、データ構造をそのまま落とし込んだモデルになっている (よく、ドメインモデル貧血症 などと言われる)。始まりがこのようなデータモデルであることは、分かりやすいし、悪くないと思う。このままでも型によって、ある程度の制約の元でこのモデルは存在し続けることができる。ただ、大抵はもっと他にこのモデルが満たし続けるべき制約が存在する。
DDD におけるドメインモデリングは、そういったモデルが満たし続けるべき制約、不変条件 (invariants) をモデルに落とし込んでいくことで、アプリケーションが不正な状態に陥ってしまうことを防ぐこと が、1つの目標になっている。 エンティティと値オブジェクト
モデルには、その同一性が重要なものとそうでないものがある。そのデータが一意に識別可能であるべきかどうかは、アプリケーションの性質による。同一性を意識する必要があるケースは、例えばそのモデルを 変更する必要がある 場合である。変更が必要ということは、変更前と変更後が存在し、それらがどちらも同一のモデルであることが判別可能である必要がある。そのような場合には「モデルはどのように一意に識別されるべきか?」を考え、モデルに落とし込む必要がある。このような同一性を意識すべきモデルは、特にエンティティと呼ばれる。
エンティティの大きな特徴は、「一意に識別可能であること」と「変化すること」の2点になる。
code:swift
struct User {
// ...
let id: String
// ...
}
一方で、同一性が重要ではないモデルも存在する。そのようなデータはどのように一意に識別させるかを考える必要はない。これだけでかなり多くの考え事から解放されることになる。同一性を意識しなくて良いということは、いつどんなタイミングで破棄したり生成したりしてもアプリケーションには何の問題もないということになる。利用したら即座に破棄しても良いし、処理間で受け渡して長い間利用し続けても良い。そのようなモデルは、数値や文字列といった値と同じように扱うことができ、値オブジェクトと呼ばれる。
値オブジェクトの大きな特徴は、「一意に識別する必要がないこと」と「不変であること (より正確には、変更の必要がない上に破棄と生成が容易であるため、不変に保つことができること)」の2点になる。
code:swift
struct UserId {
let value: String
}
struct User {
enum Sex {
case male
case female
}
// ...
let id: UserId
let sex: Sex
// ...
}
不変条件
アプリケーションには要件があり、その要件とは、大抵エンティティや値オブジェクト等のモデルの状態や振る舞いに制約を課すものになる。状態に対する制約であれば、例えば、ユーザモデルのユーザ名が何文字以上だとか、金額はn桁が上限だとか。振る舞いに対する制約であれば、例えばある値の変更は最大5回までとか。前述したデータモデルでは、これらのどの制約も簡単に破ることができてしまう。
その結果、モデルは不正な状態に陥るかもしれないし、正しいモデルでも不正な操作で不正な状態に陥らせてしまうかもしれない。そして、モデルを利用する側はモデルが不正であるかどうかを常に気にして利用する必要が出てきてしまうし、あるいはそれを忘れて不正なまま利用したことで、さらに予期しない状態を引き起こしてしまうかもしれない。
code:swift
struct UserId {
let value: String
}
struct User {
let id: UserId
enum Sex {
case male
case female
}
let name: String
let age: Int
let sex: Sex
}
状態が不正なモデルは、そもそも存在し得ないのが望ましいし、不正な状態に陥るような操作も、そもそもできないことが望ましい。そのためには、何を持って不正でないと言えるか、を示す制約を、常に守り続けさせる必要がある。このような、そのモデルの生存期間中満たされ続けるべき制約 のことを、不変条件 (invariant)と呼ぶ。
code:swift
struct User {
public init?(name: String, /* ... */) {
// 名前は5文字以上、という不変条件を保証する
guard name.count > 5 else {
return nil
}
// ...
}
}
不正な状態を許さないようなモデルは、完全コンストラクタ (Complete Constructor) とも呼ばれるようだ。
不変条件は、モデルの整合性を
トランザクションとは何か?開発者に短なトランザクションは、MySQL 等の RDBMS におけるトランザクションだが、ここでいうトランザクションとは、アプリケーションが対象とする業務上のトランザクションになる。では、業務上のトランザクションとは、どのような単位なのか? そもそもトランザクションとは、「整合性が必要なひとまとまりの処理」のことを指す。
不変条件とは、常に整合性を保っている必要のあるビジネスルールのことだ。 ... 不変条件について議論する際の整合性は、トランザクション整合性のことを指す。
集約
モデル同士は大抵、関係性のネットワークを持っている。不変条件は単一のモデルの内部のみならず、この関係性に対しても存在する。
データベースから人オブジェクトを削除しているとしよう。その人と一緒に名前や生年月日、職務経歴書はなくなる。しかし、住所はどうだろうか?同じ住所に別の人がいるかもしれない。
住所を削除すると、そうした別の人オブジェクトは、削除されたオブジェクトへの参照を持つことになる。逆に、放置すれば、データベースに不要な住所をため込んでしまう。
p123
複数のモデル間における不変条件とは、
不変条件とは、常に生合成を保っている必要のあるビジネスルールのことだ。
集約は、単なる「関連する複数のモデルの寄せ集め」ではない。
集約とは、関連するオブジェクトの集まりであり、データを変更するための単位として扱われる。
p125