『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』の読書メモ

公開日: 2024/08/20

はじめに

『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』を読んだので、せっかくなので読書メモを残します。

雑な感想

  • DDDを理解するための基礎を学べる(わりと初学者向けな気がした)
  • コードを使用して説明されているのでスラスラ読めた
  • DDD関係ないが以下の文章は心に残った

    開発者は「一事が万事」といった言葉に目を向けず、いつかリファクタリングをすべきタイミングが訪れる、という夢を信じがちです。
    同時に正しい形は見えていて、いかなるときであっても容易に書き換えられるという無謀な自信に満ち溢れています。

Chapter1: ドメイン駆動設計とは

ドメインと呼ばれる「プログラムを適用する領域」に焦点を当てた設計手法。物流システム例にすると倉庫、貨物、輸送手段などの概念が存在し、それらが物流システムのドメインに含まれる。

これらのドメインについて深く理解し問題解決に役立つものをソフトウェアに反映していく。

ドメインモデルとドメインオブジェクト

ドメインモデルとはドメインの概念をモデル化したもの。

ドメインモデルは知識を抽象化しただけ。ドメインモデルをソフトウェアで動作させるように実装表現したものをドメインオブジェクトという。

以下のように、ドメインとドメインオブジェクトはドメインモデルを媒介にお互いに繋がり影響し合う。ドメインはドメインオブジェクトに射影されるべきだし、実装中にドメインに対する鋭い洞察があったらドメインに反映されるべき。

alt text

Chapter2: 値オブジェクト

値オブジェクトとは

ドメインオブジェクトでありドメイン内のシステムの固有値の概念をモデル化したもの。以下の名前の例はわかりやすい。

alt text

要はプリミティブだけ使うんじゃなくて、より表現力豊かなシステム固有の値を使った方が良くね?という感じ。

値オブジェクトの特徴

1. 不変である

不変とは値を変更できないということ。例えばaという値をbという値に変更できないという意味。

例えば以下のようなFullNameクラスがあったとき、プロパティの値がオブジェクトのライフサイクルを通じて変わらないことを保証する。変えたいならインスタンスを作り直す必要がある。

sample.kt
data class FullName(
    val firstName: str,
    val lastName: str,
)

2. 交換可能である

以下のように代入操作によって交換することで、値オブジェクトの変更が可能。(他の手段では変更できない)

sample.kt
var fullName = FullName("Taro", "Suzuki")
fullName = FullName("Hogeo", "Suzuki")

3. 等価性によって比較される

値自身ではなく、値オブジェクトを構成する属性(インスタンス変数)で比較される。

sample.kt
val fullNameA = FullName("Taro", "Suzuki")
val fullNameB = FullName("Hogeo", "Suzuki")

fullNameA == fullNameB

値オブジェクトにする基準

  • ルールが存在するか
  • 単体で取り扱いたいか

例えば氏名には姓と名で構成されるというルールがあり、単体で取り扱われている。

値オブジェクトのメリット

  • インスタンスを作るときにバリデーションできるので不正な値が混入しない
  • 型で縛れるので誤った代入がなくせる
  • バリデーションなどのロジックが散財しない
  • など

Chapter3: エンティティ

値オブジェクトと同様にドメインオブジェクトである。値オブジェクトとの違いは同一性によって識別されるか否か。

同一性とは

例えば人間は年齢などの属性が変わったとしても別人にはならない。同一性を担保する何かが必要になる。

ソフトウェア開発に落とし込むとUserのidみたいな識別子で同一性を担保している。ageという属性が変わっても、idさえ同一であれば同じユーザとみなす。

エンティティの特徴

1. 可変である

人間の年齢などが変化するのと同様に、エンティティの属性は変更できる。
ただ必ずしも可変にする必要はない。

2. 同じ属性であっても区別される

値オブジェクトは、同じ属性であれば同じものとして扱われるがエンティティは異なる。以下のように氏名という属性が同じでも異なるものとして扱われる。(同姓同名はいるもんね)

alt text

ここで上記の人間を区別するためにidなどの識別子が使われる。

値オブジェクトとエンティティの違い

特徴 値オブジェクト エンティティ
同一性の判断基準 属性が同じかどうかで判断される 一意の識別子(ID)によって判断される
識別子 (ID) 持たない 持つ
可変性 不変 可変
例 住所 (Address)、お金 (Money)、色 (Color) ユーザー (User)、注文 (Order)、商品 (Product)
比較方法 属性の値が同じなら同じオブジェクトとみなされる 同じIDを持つエンティティは、属性が異なっていても同一と見なされる
変更時の対応 新しい値オブジェクトを作成して置き換える 同じエンティティの属性を更新する

Chapter4: 不自然さを解決する「ドメインサービス」

値オブジェクトやエンティティの振る舞いとして定義すると違和感のあるものが存在する。
例えば以下のような例。

sample.kt
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メソッドは「ユーザの重複は許さない」というドメインのルールなので、ドメインオブジェクトに定義されるべき。

ただ重複の有無を自身に問合せるのは不自然。これを解決するのがドメインサービス。

ドメインサービスとは

上記のように、エンティティや値オブジェクトに組み込むには不自然な振る舞いなどを実現するために使用する。以下のように状態を持たない「ステートレス」のサービス。

sample.kt

class UserService {
    fun exists(user: User) {
        // ユーザが重複していないか確認する処理
    }
}

ドメインモデルの振る舞いはドメインサービスに記述することは可能だが、不自然な振る舞いに限定すべき。ドメインモデルがスカスカになり「ドメイン貧血症」になってしまうから。なので可能な限りドメインサービスの使用は避ける。

Chapter6: ユースケースを実現する「アプリケーションサービス」

ユースケースを実現する。例えば以下の図のように「ユーザを登録する」「ユーザ情報を更新する」など。
alt text

アプリケーションサービスにはドメインのルールを記述しない。同じようなコードを点在させることにつながるので。

ドメインモデルを表現するだけではアプリケーションは完成しない。アプリケーションサービスがドメインオブジェクトを操作することに徹することで、ユースケースを表現する。

Chapter12: ドメインのルールを守る「集約」

データを変更するための単位として扱われるオブジェクトの集まりを集約という。集約にはルートとなるオブジェクトが存在し、すべての操作はルート越しに行われる。

以下はユーザの集約の例。
alt text

集約の外部から境界内部のオブジェクトは操作してはいけない。

sample.kt
val user = User(...)

// NG
user.name = UserName(...)

// OK
user.changeName(UserNmae(...))

上の例だとUserにchangeNameというメソッドを用意することで、引き渡された値のバリデーションなどができ、不正な値の混入を防ぐことができる。

デメテルの法則

前述の例のように、外部から内部のオブジェクトを直接操作するのではなくて、それを保持するオブジェクトに依頼する。

「オブジェクトはその直接の友達(関連するオブジェクト)としか話すべきではない」という考え方に基づいている。

Chapter13: 複雑な条件を表現する「仕様」

仕様はオブジェクトの評価をするオブジェクト。評価とは、例えば「文字列が30文字以上になっているか」など特定のビジネスルールや条件のこと。

Chapter15: ドメイン駆動設計のとびらを開こう

ユビキタス言語

共通の言葉を使いましょう的な。認識の齟齬を減らせるように。

境界付けられたコンテキスト

特定のドメインモデルやユビキタス言語の意味が一貫して使われる範囲を明確にするための境界。違うものを指しながら同じ言葉で読んでいるものがあったりする。

例えば上記の例。ユーザは使用されるコンテキストで分けられるべき。認証だけに必要なプロパティ(パスワードとか)があったり、逆にサークルだけに必要なプロパティがあるから。無理に同じオブジェクトに固執することはない。

alt text

ただ、コンテキストで細分化することによりドメイン全体がぼやけることがある。そういう場合は上図のように、コンテキストマップを作成しドメイン全体を俯瞰できると良い。