ドメインモデル貧血症を改善し、リッチドメインモデルを採用したお話し

はじめに

スマートコンテンツ事業部の兒玉です。
昨年から社内で DDD(ドメイン駆動設計) の勉強会が活発になり、ドメインモデルやモデルに振る舞いを持たせることについて学ぶ機会が増えました。
当初は理解が難しかったのですが、実際の業務で新規リソースを立ち上げる中で、「モデルに振る舞いを記述することの利便性」を実感しました。この記事では、その気づきを具体例を交えて共有します。
※この記事は「エムティーアイ Blog Summer 2025」の 8/22 分の記事です。

ドメインモデル貧血症とは?

ドメインモデル貧血症については以下のように語られており、ドメインモデルがgetter/setterしか保持しておらずオブジェクトに対する振る舞いを保持していないため、データの入れ物としてのみ機能するドメインモデルになってしまっている状態を指します。

martinfowler.com

bliki-ja.github.io

貧血症モデルでは、典型的に以下のような構成になります。 今回、貧血症を補っているService層を減らしModelの振る舞いで完結するように改善を促すことで、その差分から何が嬉しいのかについて共有できればと思います。

例:

src/
 ├─ Controllers/
 │    ├─ UserController.cs      // APIエンドポイント
 │    └─ OrderController.cs
 │
 ├─ Models/
 │    ├─ Entities/
 │         ├─ User.cs           // DBマッピング用Entity(getter/setterのみ:貧血症)
 │         └─ Order.cs
 │
 ├─ Services/
 │    ├─ UserService.cs         // ビジネスロジック集中(貧血症を補う)
 │    └─ OrderService.cs
 │
 ├─ Repositories/
 │    ├─ IUserRepository.cs
 │    └─ UserRepository.cs

モデルの振る舞いとは

モデルの振る舞いについては、それだけで本が一冊出てるので、ここでは簡単な説明に留めます。
わかりやすいように、振る舞いの無い貧血症モデルを参考に説明します。

貧血症モデル

まずは、貧血症モデルの例です。
特徴としては、以下があります。

貧血症モデルの特徴

  • 状態(プロパティ)だけを持つ
  • ビジネスルールや操作はService層やController層で処理する

Entities/User.cs(典型的なgetter/setterのみ)

namespace Project.Entities
{
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Point { get; set; }
    }
}

例えば、ポイント付与処理は以下のように、Service層で実装することが多いです。

// Service層でポイント付与(モデルはただのデータ保持)
public void AddPoint(User user, int points)
{
    user.Point += points;
}

この場合、呼び出し方は以下のようになります。

var user = userRepository.FindById(userId);
userService.AddPoint(user, 100);
userRepository.Save(user);

呼び出し方の特徴として、
userを取得して、userServiceに渡し、再びuserを使う」というように、モデルが単なるデータの袋なので、変数を保持しながら複数メソッドを回すコードになりやすいといった特徴があります。
責務が分際しているため、モデルを見てもロジックが分からず、保守がしづらいという現実があります。

リッチドメインの場合

貧血症を解消したモデル(=リッチドメイン)だと以下のような例になります。
改善点の特徴としては以下です

リッチドメインモデルの特徴

  • 不変条件を守れる
    • ポイントはマイナスを許さない
  • 自己完結的な振る舞い
    • AddPointで必ずバリデーションする
  • 外部で直接Pointを書き換えられない (private set)

Entities/User.cs(振る舞いを持たせる)

namespace Project.Domain.Users
{
    public class User
    {
        public int Id { get; private set; }
        public string Name { get; private set; }
        public int Point { get; private set; }
    
        public void AddPoint(int points)
        {
            if (points <= 0)
                throw new ArgumentException("Points must be positive.");
    
            Point += points;
        }
    }
}

この場合、呼び出し方は以下のようになります。

var user = userRepository.FindById(userId);
user.AddPoint(100);
userRepository.Save(user);

呼び出し方の特徴ですが、
Service層を挟む必要がなくなったので、変数userの操作だけで完結し読みやすく、Modelに責務が集約されたため、「Userがポイントを加算する(user.AddPoint(100);)」という自然な表現でコードを記述することが可能になります。

貧血症モデルとリッチドメインモデルの比較

ここまでのまとめです。
貧血症モデルでは以下の特徴がありました。(あえてデメリットとは記載しません)

  • userService.AddPoint(user, points);のように、常にuserを渡す必要がある。
  • モデルが受動的なので、操作するたびにサービス呼び出しを考えないといけない。

リッチドメインモデルでは以下の特徴がありました。

  • user.AddPoint(points);モデルが自律的なので、直感的。
  • 呼び出し元がオブジェクト指向的で手続き的な冗長さが消える。

リッチドメインモデルを採用して得たメリットですが、すぐに効果を感じたものとしては以下のようなものがありました。

  1. コード補完の精度が高まる
    • user. と打てば関連する操作が見える
  2. 命名の一貫性がある
    • user.AddPoint()は、「ユーザーにポイントを追加する」という意味が明確になっている
    • サービス層だとAddPoint(user, points)→ 第一引数に「誰に対してか」を毎回指定するので、冗長となっていた

最後に

リッチドメインモデルを採用し実装リリースまでできたというのは、とても良い経験になりました。 私のチームではおそらく初の試みだったので、実際にはうまくいかない箇所もあり、例えば Service層のあり方定義などあまり解決策が見えない部分もありました。

実装の範囲や役割分担について悩むこともありましたが、チーム内での議論やリーダーとの調整を通じて、より良い形を模索できました。初めての取り組みでしたが、学びの多い経験になったと思います。 今後取り入れる方への、初学の手助けになれば幸いです。