はじめに
今回はポイント管理システムをテーマに
DDD×クリーンアーキテクチャでサンプルコードを作ったので、
それを使いながら、
エンティティと値オブジェクトとは??
どう使い分けるか??についてまとめました。
動画も作ったのでご覧ください。
エンティティと値オブジェクトのサンプルコード
今回テーマにしているポイントシステムでは、
店舗からレシート情報とカード、ポイント利用などを連携され、
付与ポイントの加算、ポイント利用の減算などの
ポイント管理ができるシステムを想定しています。
まずは値オブジェクトのサンプルコードを見てみましょう。
data class PaymentDate(val value: LocalDate) {
init {
val today = LocalDate.now()
if (value.isAfter(today) || value.isBefore(today.minusMonths(1))) throw IllegalArgumentException("支払日が不正です(当日〜過去1ヶ月以内である必要があります)")
}
}
ポイント管理システムにおいて支払日には以下のような仕様としています。
- 店舗で商品を購入した日が支払日となるため未来日になることはない
- 購入時にポイントカードを忘れた場合1ヶ月以内にレシートを持参すればその日を支払日としてポイント付与する
そのため、initで上記仕様を表現しており、
ポイントシステムで取り扱うべき支払日の範囲外の場合は、
値オブジェクトを生成できないようにガード節を設けています。
次にエンティティのサンプルコードを見ていきましょう。
class PaymentMember(
val memberCode: MemberCode,
private val remainingPoints: Point
) {
fun usePoints(point: Point): PaymentMember {
if (remainingPoints.value < point.value) throw IllegalArgumentException("ポイントが不足しています")
return PaymentMember(memberCode, Point(remainingPoints.value - point.value))
}
}
こちらはmemberCodeで支払メンバーが一意に特定できます。
remainingPointsを不変とし、
副作用が意図せず発生しないように
動作を安定させる工夫をしています。
そのため支払メンバーがポイント利用した際は
新たにエンティティを生成しなおす必要があります。
支払という文脈におけるメンバーを「支払会員」エンティティでは表現しており、
支払に関係のないメンバーの名称や住所、生年月日などはインスタンス変数から排除しています。
もし、
新規会員を登録する場合は支払会員とは別のエンティティを用意し、
そちらにはメンバーの名称や住所、生年月日、残ポイント(0ポイント)を
インスタンス変数として用意します。
新規会員登録と支払登録の文脈で会員エンティティを無理やり使い回すのは
混乱を招く危険があるので気をつけましょう!!
エンティティと値オブジェクトの使い分け方
エンティティと値オブジェクトは、
どちらもドメインモデルを表現するオブジェクトです。
これらの見分け方は同一判定をどう行うか??です。
先ほどの「支払日」値オブジェクトは、
値オブジェクトが保持するLocalDate型のvalueという属性が
同一なら支払日は同一と判定し、
異なれば支払日は異なると判定します。
一方
「支払会員」エンティティは、
会員コードで会員が同一かどうかを判定します。
例えば、1人の人間が複数アカウントを持っていたとしても、
ポイント管理システムでは複数アカウント全てに異なる会員コードを発番し、
別会員として扱うことになります。
では使い分けについてですが、
僕の場合は、例えば今回のポイント管理システムの
支払登録をドメインモデリングをこんな感じでしたのですが、
![](https://poppingcarp.com/wp-content/uploads/2023/10/ddd-entity-sample-program_rewrite2.png)
この方向性でまずはエンティティを作ります。
値オブジェクトは最初は作らずプリミティブ型とします。
(例えば支払日はLocalDate型で表現します。)
作っていくうちに、
支払日のチェックを何箇所かで行い始めたら、
1箇所で支払日のチェックができないか??
を検討して値オブジェクトとして別クラスに括り出す。
という流れで整理していくことが多いです。
また、エンティティを一意に特定するためのコードやIDなどは、
業務ロジック関係なく値オブジェクトを用意する場合もあります。
プリミティブ型のStringで会員コード、レシート番号など全て表現していると、
間違ってコードを利用しても気づけませんが、
値オブジェクトで明確にしておくと、
間違えて利用するとコンパイルエラーで気付けるのが理由です。
(登場する全ての属性を何も考えずに値オブジェクト用意するなどは
コスパが悪いと自分は思っています)
値オブジェクトの重要性についてまとめた記事があるので
そちらも参照ください。
![](https://poppingcarp.com/wp-content/uploads/2023/02/ange_of_value_2-160x160.png)
今回のテーマについては以上です。
DDD実践のためのきほんシリーズのまとめ記事を作りました。
是非こちらもご覧ください。
![](https://poppingcarp.com/wp-content/uploads/2022/02/ddd-summary_rewrite_1-160x160.png)
参考文献
第3章 エンティティ/値オブジェクト
ドメイン駆動設計 サンプルコード&FAQ
第6章 ドメイン層の実装
ドメイン駆動設計 モデリング/実装ガイド
Chapter1 小さくまとめてわかりやすくする
現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法
Chapter2 システム固有の値を表現する「値オブジェクト」
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本
Chapter3 ライフサイクルのあるオブジェクト「エンティティ」
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本