公開日: 2024/08/20
『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』を読んだので、せっかくなので読書メモを残します。
開発者は「一事が万事」といった言葉に目を向けず、いつかリファクタリングをすべきタイミングが訪れる、という夢を信じがちです。
同時に正しい形は見えていて、いかなるときであっても容易に書き換えられるという無謀な自信に満ち溢れています。
ドメインと呼ばれる「プログラムを適用する領域」に焦点を当てた設計手法。物流システム例にすると倉庫、貨物、輸送手段などの概念が存在し、それらが物流システムのドメインに含まれる。
これらのドメインについて深く理解し問題解決に役立つものをソフトウェアに反映していく。
ドメインモデルとはドメインの概念をモデル化したもの。
ドメインモデルは知識を抽象化しただけ。ドメインモデルをソフトウェアで動作させるように実装表現したものをドメインオブジェクトという。
以下のように、ドメインとドメインオブジェクトはドメインモデルを媒介にお互いに繋がり影響し合う。ドメインはドメインオブジェクトに射影されるべきだし、実装中にドメインに対する鋭い洞察があったらドメインに反映されるべき。
ドメインオブジェクトでありドメイン内のシステムの固有値の概念をモデル化したもの。以下の名前の例はわかりやすい。
要はプリミティブだけ使うんじゃなくて、より表現力豊かなシステム固有の値を使った方が良くね?という感じ。
不変とは値を変更できないということ。例えばa
という値をb
という値に変更できないという意味。
例えば以下のようなFullName
クラスがあったとき、プロパティの値がオブジェクトのライフサイクルを通じて変わらないことを保証する。変えたいならインスタンスを作り直す必要がある。
data class FullName(
val firstName: str,
val lastName: str,
)
以下のように代入操作によって交換することで、値オブジェクトの変更が可能。(他の手段では変更できない)
var fullName = FullName("Taro", "Suzuki")
fullName = FullName("Hogeo", "Suzuki")
値自身ではなく、値オブジェクトを構成する属性(インスタンス変数)で比較される。
val fullNameA = FullName("Taro", "Suzuki")
val fullNameB = FullName("Hogeo", "Suzuki")
fullNameA == fullNameB
例えば氏名には姓と名で構成されるというルールがあり、単体で取り扱われている。
値オブジェクトと同様にドメインオブジェクトである。値オブジェクトとの違いは同一性によって識別されるか否か。
例えば人間は年齢などの属性が変わったとしても別人にはならない。同一性を担保する何かが必要になる。
ソフトウェア開発に落とし込むとUser
のid
みたいな識別子で同一性を担保している。age
という属性が変わっても、id
さえ同一であれば同じユーザとみなす。
人間の年齢などが変化するのと同様に、エンティティの属性は変更できる。
ただ必ずしも可変にする必要はない。
値オブジェクトは、同じ属性であれば同じものとして扱われるがエンティティは異なる。以下のように氏名という属性が同じでも異なるものとして扱われる。(同姓同名はいるもんね)
ここで上記の人間を区別するためにid
などの識別子が使われる。
特徴 | 値オブジェクト | エンティティ |
---|---|---|
同一性の判断基準 | 属性が同じかどうかで判断される | 一意の識別子(ID)によって判断される |
識別子 (ID) | 持たない | 持つ |
可変性 | 不変 | 可変 |
例 | 住所 (Address)、お金 (Money)、色 (Color) | ユーザー (User)、注文 (Order)、商品 (Product) |
比較方法 | 属性の値が同じなら同じオブジェクトとみなされる | 同じIDを持つエンティティは、属性が異なっていても同一と見なされる |
変更時の対応 | 新しい値オブジェクトを作成して置き換える | 同じエンティティの属性を更新する |
値オブジェクトやエンティティの振る舞いとして定義すると違和感のあるものが存在する。
例えば以下のような例。
data class User(
val id: UserId
val name: UserName
) {
fun exists(user: User): Boolean {
// ユーザが重複していないか確認する処理
}
}
value class UserId(val value: Int)
value class UserName(val value: String)
...
val user = User(UserId(0),UserName("Suzuki"))
user.exists(user)
exists
メソッドは「ユーザの重複は許さない」というドメインのルールなので、ドメインオブジェクトに定義されるべき。
ただ重複の有無を自身に問合せるのは不自然。これを解決するのがドメインサービス。
上記のように、エンティティや値オブジェクトに組み込むには不自然な振る舞いなどを実現するために使用する。以下のように状態を持たない「ステートレス」のサービス。
class UserService {
fun exists(user: User) {
// ユーザが重複していないか確認する処理
}
}
ドメインモデルの振る舞いはドメインサービスに記述することは可能だが、不自然な振る舞いに限定すべき。ドメインモデルがスカスカになり「ドメイン貧血症」になってしまうから。なので可能な限りドメインサービスの使用は避ける。
ユースケースを実現する。例えば以下の図のように「ユーザを登録する」「ユーザ情報を更新する」など。
アプリケーションサービスにはドメインのルールを記述しない。同じようなコードを点在させることにつながるので。
ドメインモデルを表現するだけではアプリケーションは完成しない。アプリケーションサービスがドメインオブジェクトを操作することに徹することで、ユースケースを表現する。
データを変更するための単位として扱われるオブジェクトの集まりを集約という。集約にはルートとなるオブジェクトが存在し、すべての操作はルート越しに行われる。
以下はユーザの集約の例。
集約の外部から境界内部のオブジェクトは操作してはいけない。
val user = User(...)
// NG
user.name = UserName(...)
// OK
user.changeName(UserNmae(...))
上の例だとUser
にchangeName
というメソッドを用意することで、引き渡された値のバリデーションなどができ、不正な値の混入を防ぐことができる。
前述の例のように、外部から内部のオブジェクトを直接操作するのではなくて、それを保持するオブジェクトに依頼する。
「オブジェクトはその直接の友達(関連するオブジェクト)としか話すべきではない」という考え方に基づいている。
仕様はオブジェクトの評価をするオブジェクト。評価とは、例えば「文字列が30文字以上になっているか」など特定のビジネスルールや条件のこと。
共通の言葉を使いましょう的な。認識の齟齬を減らせるように。
特定のドメインモデルやユビキタス言語の意味が一貫して使われる範囲を明確にするための境界。違うものを指しながら同じ言葉で読んでいるものがあったりする。
例えば上記の例。ユーザは使用されるコンテキストで分けられるべき。認証だけに必要なプロパティ(パスワードとか)があったり、逆にサークルだけに必要なプロパティがあるから。無理に同じオブジェクトに固執することはない。
ただ、コンテキストで細分化することによりドメイン全体がぼやけることがある。そういう場合は上図のように、コンテキストマップを作成しドメイン全体を俯瞰できると良い。