貧血モデルとは何か
貧血モデル(Anemic Domain Model)は
「フィールド(データ)だけ持っていて、自分の振る舞い(ビジネスロジック)をほとんど持たないオブジェクト」
がシステム中にあふれている設計スタイルのことです。
クラスは getter / setter だらけ。
業務の本質的な処理は、Service クラスやユーティリティクラスにベタっと書かれている。
このような状態を「ドメインモデルが貧血状態だ」と表現します。
名前だけ「User」や「Order」なのに、やっていることは事実上「テーブルの行を運ぶ DTO」に近い。
これが貧血モデルです。
典型例で見る「貧血モデル」
データだけ持つ User クラス
まず、よくある User クラスを見てみましょう。
final class User {
private Long id;
private String name;
private String email;
private int point;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getPoint() {
return point;
}
public void setPoint(int point) {
this.point = point;
}
}
Java一見「ちゃんとカプセル化されている」ように見えますが、
この User は自分では何のルールも守っていません。
ポイントを増やす、メールアドレスの形式を保証する、といった振る舞いは一切なく、
ただの入れ物です。
業務処理はどうなっているかというと、たとえばこんな Service に集まります。
final class UserService {
void addPoint(User user, int added) {
if (added < 0) {
throw new IllegalArgumentException("マイナスはだめ");
}
int newPoint = user.getPoint() + added;
if (newPoint > 10_000) {
newPoint = 10_000;
}
user.setPoint(newPoint);
}
}
Javaポイントに関するルールは User ではなく UserService が抱え込んでいます。
User は「自分のポイントを正しいルールで更新する」ことができず、
外側の誰かにやってもらうしかない状態です。
これが「貧血モデル」の典型です。
なぜ貧血モデルが問題視されるのか(重要)
オブジェクト指向の「データと振る舞いの一体化」が崩れる
オブジェクト指向の基本は
「データ(状態)と、それに対する振る舞い(メソッド)を一体としてクラスにする」
ことです。
本来の姿はこうです。
「User は自分のポイントを正しく加算できる」
「Order は自分の合計金額を計算できる」
「Account は自分の残高を更新できる」
にもかかわらず、貧血モデルでは
User は何もできないただの入れ物
Order も何もできないただの入れ物
それらをいじる処理は全部 Service に集約
という形になり、「データ」と「振る舞い」が分離されてしまいます。
その結果、ビジネスルールがクラスに紐づかず、
「どこを読めばこのルールが分かるのか」が極端に分かりづらくなります。
ビジネスルールがサービスクラスに散らばる
貧血モデルのシステムでは、Service クラスがどんどん巨大化します。
UserService にポイントロジック、メール変更ロジック、退会ロジックなどが全部入る。
OrderService に注文確定ロジック、キャンセルロジック、送料計算ロジックなどが全部入る。
クラス名だけ見ればそれっぽいのですが、実際の中身は
「手続き的な if と計算が詰め込まれた神クラス」になりがちです。
新しい仕様変更が入るたびに、その巨大 Service を開いて
「似ている処理を探してコピペして、if を足す」という作業が増えます。
ルールごとにクラスが分かれていないので、変更の影響範囲も読みにくくなります。
不正な状態のオブジェクトを簡単に作れてしまう
先ほどの User を思い出してください。
User u = new User();
u.setName(null);
u.setPoint(-100);
u.setEmail("aaa"); // メール形式でもない
Javaこんな User を簡単に作れてしまいます。
User クラス自身は「それはダメ」と言ってくれません。
正しい状態を保証する責任がどこにもないため、
「どこかで気をつけてチェックしているはず」といった“人間頼み”の設計になります。
見落としがバグに直結する構造です。
「貧血じゃないモデル」はどう違うのか(対比で理解する)
User が自分のルールを知っている形
同じ User を、リッチドメインモデル寄りに書き直してみます。
final class User {
private final Long id;
private String name;
private String email;
private int point;
User(Long id, String name, String email, int point) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名前は必須");
}
if (!email.contains("@")) {
throw new IllegalArgumentException("メール形式がおかしい");
}
if (point < 0) {
throw new IllegalArgumentException("ポイントは0以上");
}
this.id = id;
this.name = name;
this.email = email;
this.point = point;
}
void changeEmail(String newEmail) {
if (!newEmail.contains("@")) {
throw new IllegalArgumentException("メール形式がおかしい");
}
this.email = newEmail;
}
void addPoint(int added) {
if (added < 0) {
throw new IllegalArgumentException("マイナスは不可");
}
int newPoint = this.point + added;
if (newPoint > 10_000) {
newPoint = 10_000;
}
this.point = newPoint;
}
int point() {
return point;
}
}
Javaここでは、ユーザの
生成時の妥当性(名前必須、メール形式、ポイント >= 0)
メール変更時のルール
ポイント加算時のルール
を User 自身が責任を持っています。
この状態なら、
「ポイントロジックを変えたい」と思ったとき、
見るべき場所は User クラスの addPoint だとすぐ分かります。
外側のサービスは、User の「使い方」を組み合わせるだけの薄いクラスになりやすいです。
final class UserService {
void register(User user) {
// リポジトリに保存など。
// ここでポイント計算やメールの妥当性チェックはしない。
}
}
Javaこの違いが、貧血モデルとリッチドメインモデルの大きな差です。
「全部リッチにすべき?」というよくある疑問
ここで悩ましいのは、「じゃあ全部のクラスをリッチにすべきなのか?」という疑問です。
現実的には、そうとも限りません。
単なる入出力のための DTO や、ビューとのやりとりのためだけのクラスは、
「本当にただのデータの入れ物」で良い場面もあります。
大事なのは
業務のルール(ドメイン知識)を持つべきクラスまで「ただの DTO」にしてしまっていないか
本来そこにあるべき振る舞いが、Service や Util に追い出されていないか
という視点です。
「このクラスに、業務的に意味のあるルールはあるか?」
「そのルールが if や計算式として別のクラスに散っていないか?」
これを自分に問いかけてみると、
どこが貧血で、どこをリッチにすべきかが見えてきます。
貧血モデルになりがちなパターンと、その背景(重要な深掘り)
ORM やテーブル設計から逆算してクラスを作っている
JPA / MyBatis などの ORM を使うと、つい「テーブルの行」をそのままクラスにマッピングしたくなります。
users テーブル
columns: id, name, email, point
だから User エンティティもそのまま
フィールド+getter/setter だけにしてしまう、という流れです。
このとき、
「このクラスは実際のビジネスの中でどんなルールを持つべきか?」
という視点を忘れやすくなります。
テーブル設計や DB の都合ではなく、
ドメイン(業務)の都合を基準に「どんな振る舞いを持たせるか」を考えるのが、
貧血モデルから抜ける第一歩です。
三層アーキテクチャの誤解
プレゼンテーション層、アプリケーション層、ドメイン層、インフラ層
といったレイヤを意識するのは良いことですが、
これを誤解して
ドメイン層にはフィールドと getter/setter だけ
業務ロジックはすべてアプリケーションサービス層に書く
という状態にしてしまうと、「見た目だけレイヤード、実態は手続き型」という構造になります。
ドメイン層には、ビジネスルールの中核を実装したクラスがいるべきです。
アプリケーション層はそれらを「どの順番でどう呼ぶか」を組み立てる役目に留めると、
コードの責務分担が一気にクリアになります。
まとめ:貧血モデルかどうか、どこで見分けるか
貧血モデルの本質は
「ドメインのオブジェクトが、自分のルールを知らず、何もできないただの入れ物になっていること」
です。
クラスを眺めてみて、
ほぼ getter / setter しかない
業務的に意味のあるメソッドがない
不正な状態のチェックをまったくしていない
一方で、Service クラスが if と計算だらけで太っている
と感じるなら、そのあたりは「貧血気味」の可能性が高いです。
逆に、
そのクラスの責務に関するルールが、そのクラスのメソッドとして書かれているか
「この仕様変更なら、このクラスのこのメソッドを見ればいい」と言えるか
という観点で見ていくと、どこをリッチにすべきかが見えてきます。
