はじめに
機能嫉妬(Feature Envy) とは、
あるクラスのメソッドが「自分のクラスのフィールド」ではなく、
他のクラスのデータばかり利用して処理している状態 を指します。
例:
-
OrderService
がOrder
の内部データを直接操作して計算している -
UserService
がUser
のフィールドを読み取って、バリデーションや判定をしている
→ 本来は 利用しているクラス側に処理を移すべき であり、カプセル化違反のサインです。
24.1 特徴
- あるメソッドが 他のクラスの getter を大量に呼び出す
- 自分のフィールドをほとんど使わない
- ビジネスロジックが ドメインオブジェクトではなくサービス層に偏在
- 「このメソッドは別の場所にあるべきでは?」と感じる
24.2 解決手法
-
メソッド移動(Move Method)
→ 他クラスのデータに依存しているメソッドを、そのクラスに移動 -
データと振る舞いの統合
→ データを持つクラスに、そのデータを扱うロジックも持たせる -
ドメインモデル化
→ サービスに偏ったロジックをエンティティや値オブジェクトに移す
24.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
}
}
-
calculateTotal
は Order のデータばかり使っており、OrderService 自身の状態は不要 - このロジックは
Order
にあるべき
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)」と「振る舞い(演算)」を統合。
→ OrderService
にあった処理は不要になった。
24.4 実務での指針
- getter の連発は Feature Envy のサイン
- ロジックは 最も関連の深いクラス に配置する
- サービスにロジックを集めすぎると「貧血モデル」になりやすい
- Kotlin では data class + value class を組み合わせ、データと振る舞いを一体化できる
まとめ
- Feature Envy は「他のクラスのデータばかり欲しがるメソッド」
- 解決策は Move Method / 値オブジェクト化 / ドメインモデル化
- 基本思想:データと振る舞いは一緒にあるべき