はじめに
貧血ドメインモデル(Anemic Domain Model) とは、
ドメインオブジェクトが フィールド(データ)だけを持ち、振る舞い(ビジネスロジック)を持たない状態 を指します。
結果として:
- ロジックは サービス層 に集中
- エンティティや値オブジェクトは 単なるデータ入れ物 になってしまう
- オブジェクト指向の利点(カプセル化・一貫性)が失われる
29.1 特徴
- エンティティ/値オブジェクトが getter/setter しか持たない
- ビジネスルールはすべて Service クラス に押し込められている
- 集約の整合性が 呼び出し側任せ になっている
- 「データベースのテーブル設計」とほぼ同じ構造になっている
29.2 解決手法
-
ドメインモデルに振る舞いを移す(Move Method)
→ データを扱うロジックは、そのデータを持つクラスに閉じ込める -
不変条件をクラス内に保持
→ クラス外で制御していた整合性チェックをオブジェクト内に取り込む -
DDD(ドメイン駆動設計)の導入
→ エンティティ/値オブジェクトにビジネスルールを持たせ、サービスは調整役に留める
29.3 Kotlin 例
Before:貧血ドメインモデル
data class Order(val items: List<Double>, val customerLevel: Int)
class OrderService {
fun calculateTotal(order: Order): Double {
val subtotal = order.items.sum()
return if (order.customerLevel > 3) subtotal * 0.9 else subtotal
}
}
-
Orderはただのデータ入れ物 - 計算ロジックはすべて
OrderServiceに集中している
After①:ドメインモデルに振る舞いを統合
data class Order(val items: List<Double>, val customerLevel: Int) {
fun calculateTotal(): Double {
val subtotal = items.sum()
return if (customerLevel > 3) subtotal * 0.9 else subtotal
}
}
→ Order が「合計計算」という責務を担い、自己完結
After②:値オブジェクトを導入
@JvmInline
value class Money(val value: Double) {
init { require(value >= 0) { "金額は正数でなければならない" } }
operator fun plus(other: Money) = Money(this.value + other.value)
operator fun times(rate: Double) = Money(this.value * rate)
}
data class Order(val items: List<Money>, val customerLevel: Int) {
fun calculateTotal(): Money {
val subtotal = items.reduce { acc, m -> acc + m }
return if (customerLevel > 3) subtotal * 0.9 else subtotal
}
}
→ 金額の制約を Money に閉じ込め、Order のロジックも安全になる。
29.4 実務での指針
- Service クラスが太っていないか? を常に確認
- ロジックは データを持つクラス に置く(データ+振る舞いの一体化)
- Kotlin では data class + value class を積極的に活用し、
不変性と意味付けをクラス内に閉じ込める - DDD を導入する場合、エンティティ/値オブジェクトにルールを寄せる
まとめ
- Anemic Domain Model は「オブジェクト指向の形骸化」
- 解決策は Move Method / Value Object / DDD 実践
- 基本思想:データとロジックは同じ場所にあるべき