実務Webアプリ開発編です。前回は Part III(アーキテクチャ概論)の最初の記事として、クリーンアーキテクチャの全体像を整理しました。

【C#/Blazor】実務Webアプリ開発編 (15)クリーンアーキテクチャ入門 ~4層構成の役割と依存方向のルール~ 実務Webアプリ開発編です。前回まではPart II(要件定義と設計)で、MentorApp の要件定義と設計を通して、何を作るかを整...

今回はその続きとして、ドメイン駆動設計(DDD)を学びます。MentorApp を題材にしながら、業務ルールをコードでどう表すかを見ていきましょう。

以下のような方に役立つ内容となっています。

  • DDD を学び始めたが、何がうれしい考え方なのかまだ腹落ちしていない
  • データは設計できても、業務ルールをどこに置くべきか迷っている
  • C# BlazorでDDDを導入したいが、どこから手をつけてよいかわからない

以下のようなMentorAppを題材として進めます。

GitHubにドキュメント・コードの一式があります。

プロ太

DDDは、業務の意味とルールをコードに宿すための考え方です。クリーンアーキテクチャとあわせて見ていきましょう!

なぜ DDD が必要なのか

前回のクリーンアーキテクチャは、責務をどの層に分けるかという整理でした。DDD は主に中心(Domain層)で、業務の意味をどう表すかに焦点をあてます。

DDDでは、たとえば次のような課題を扱います。

  • 課題①:業務で使う言葉と、コード上の名前が対応していない
    解消の考え方:業務で使う言葉を、そのままコードの名前に使う
  • 課題②:業務ルールが呼び出し側に散らばり、変更に弱くなる
    解消の考え方:業務ルールを、それを担当するオブジェクトに持たせる

DDDによるこれら課題解決についてざっくりつかむため、MentorApp の「相談トピックをクローズする」「メッセージを投稿する」操作を例に見てみましょう。

ここではDDDの考え方をざっくりつかむために、実際の設計よりも単純化した例で見ていきます。

課題①:業務の言葉とコードのずれをどう解消するか

まずは、業務で使う言葉とコードの名前が対応していない例です。

// Before
if (topic.Status != TopicStatus.Closed)
{
    topic.Status = TopicStatus.Closed;
}
// After
topic.Close();

Before でも「クローズ状態へ変える」ことは分かりますが、業務で実際に話しているのは「相談トピックをクローズする」という操作です。

After のように Close() と書けると、仕様で使う言葉とコードの名前がそろい、意図を読み取りやすくなります

課題②:業務ルールの散在をどう解消するか

次は、ユーザーのロールを変更できる条件が呼び出し側に書かれてしまっている例です。

// Before
var hasActiveMentorship = await mentorshipRepository
    .HasAnyActiveMentorshipByUserIdAsync(user.Id);

if (!hasActiveMentorship)
{
    user.ChangeRole(newRole);
}
// After
await roleChangeValidationService
    .ValidateAsync(user, newRole, mentorshipRepository);
user.ChangeRole(newRole);

Before では、「Active なメンタリング関係があるか」という条件を、ロール変更のたびに呼び出し側で調べています。

同じロール変更が別の画面や処理から呼ばれるようになると、この条件が各所にコピーされやすくなります。

After のように ValidateAsync(...) のような形へ寄せると、UserMentorship にまたがる判定を、呼び出し側ではなく業務ルールを担当する側へまとめて管理できます

このシリーズで特に見る点

このシリーズでは、特に課題②の解消アプローチに注目して進めます。つまり、業務ルールをどのオブジェクトに持たせると分かりやすく、変更にも強くなるかを見ていきます。

プロ美

課題①は Close() のように業務の言葉で操作を表し、課題②は複数の対象にまたがるルールをオブジェクトでまとめているんだね。

プロ太

その通りです。ここで説明した例は、考え方をつかみやすいようにDDDのエッセンス部分をかなり単純化して見ています。

DDD は実際にはこうした課題に対して、もう少し体系的に設計の手がかりを与えてくれます。

DDD には大きく、「戦略的 DDD」 と「戦術的 DDD」 があります。

  • 戦略的 DDD は、課題①に対応します。
    仕様書とコードで使う言葉をそろえることを重視し、境界づけられたコンテキストユビキタス言語はこちらの概念です。
  • 戦術的 DDD は、課題②に対応します。
    業務の概念を役割ごとに整理し(エンティティ・値オブジェクト・集約など)、それぞれの担当オブジェクトに業務ルールを持たせます。

このシリーズで主に扱うのは、コードに落とし込みやすい「戦術的 DDD」です。

戦術的 DDDについては、Microsoftの記事も参考になります。例はマイクロサービスですが、基本概念の整理には役立ちます。

DDD の考え方と基本要素をつかむ

前セクションで確認した課題②の解決アプローチを実現するために、DDD では業務の概念をどう分類し、それぞれにどんな役割を持たせるかを見ていきましょう。

DDD では、そのアプリが扱う業務の世界全体をドメインと呼びます。MentorApp であればメンタリング業務の仕組み全体がこれにあたります。

このドメインを実装で表すための要素として、値オブジェクトエンティティ集約ドメインサービスドメインイベントが中心になります。
(注意:ドメインイベントはMentorAppでは使用していません)。

要素何を表すか具体的なイメージ
値オブジェクト値そのものに意味があるものメールアドレス、金額、期間など
エンティティID を持ち、同一性で見分けるもの同じ人・同じ注文のような対象
集約関連するエンティティをまとめて一貫性を守る単位1回の変更で一貫して扱いたい範囲
ドメインサービス1つの対象に閉じにくい業務ルール複数の対象をまたぐ判定処理
ドメインイベント業務上の出来事・変化の通知注文が確定した・状態が変わったなど

値オブジェクトは、値そのものが意味の中心です。たとえばメールアドレスは、「誰のものか」より正しい形式の値かが重要になります。

エンティティは、ID で区別される存在です。名前や属性が少し変わっても、同じものとして追い続けます。

要件定義・設計におけるデータモデリングで洗い出した実体の多くが、このエンティティに対応します。

迷ったときは、ID で区別して追い続けたいならエンティティ、値そのもので扱えれば値オブジェクトと考えると判断しやすくなります。

集約は、関連するエンティティをひとまとめにして、矛盾なく変更したい範囲を決める考え方です。まとまりの入口を一つに決めることで、その内部のルールを守りやすくします。

ドメインサービスは、1つのエンティティだけでは表しにくいルールを置く場所です。複数の対象を見比べて判定するときに役立ちます。

プロ太

課題②の例で登場した「roleChangeValidationService
.ValidateAsync」はドメインサービスの例です。

ドメインイベントは、業務上の出来事や状態変化を表すオブジェクトです。「注文が確定された」などの事実を、他のコンポーネントへ通知するときに使います。

MentorApp では DDD がどう形になっているか

ここからはMentorApp が DDD の設計方針をどのように具体化しているかをざっくり確認します。ポイントは、Domain プロジェクトが中心になっていることです。

MentorApp のコードでは、DDD に関わる要素が次のように整理されています。前のセクションで見た役割が、そのままコード上の置き場に対応しています。

src/
  MentorApp.Domain/
    Models/              // エンティティ・値オブジェクト・集約
      Users/
        User.cs          // エンティティ・集約ルート
        Email.cs         // 値オブジェクト
        …
      Mentorships/
        Mentorship.cs    // エンティティ・集約ルート
        …
      Topics/
        Topic.cs         // エンティティ・集約ルート
        Message.cs       // エンティティ
        …
      Shared/            // 共通の値オブジェクトなど
    Services/            // ドメインサービス(複数集約をまたぐルール)
      MentorshipDuplicationCheckService.cs  // ドメインサービス

要件定義・設計編のデータモデリングで洗い出した4つの実体(User・Mentorship・Topic・Message)が、エンティティとして Domain プロジェクトに置かれています。

これら4エンティティは、以下の3つの集約にまとまっています。集約ルートは集約に対して操作を行うときの入り口です。

  • User(集約ルート)
  • Mentoship(集約ルート)
  • Topic(集約ルート)、Message

「Topic、Message」の集約でTopicを入口にするのは、メッセージ投稿に「クローズ済みトピックには投稿できない」というルールがあるからです。

Topic.csPostMessage() でこの判定が行われ、Message を単独で直接生成することはできません。こうして、投稿ルールを Topic 側でまとめて守る形になっています。

また、値オブジェクトの例は Email.cs で、文字列をそのまま持たず有効なメールアドレスという意味を型で表しています。

ドメインサービスの例は MentorshipDuplicationCheckServiceRoleChangeValidationService です。

これらは、Mentor と Mentee の組み合わせや、Active な関係があるときのロール変更可否など、複数の対象をまたぐ判定を受け持っています。

プロ美

要件定義や設計で描いていた概念が、そのままコードの役割として形になっているんだね。

プロ太

そのとおりですね。Part IV以降では、それぞれを C# でどう実装しているかを順番に詳しく見ていきます。

まとめ

DDD は、データをどう保存するかだけでなく、このアプリで何を守るべきかをコードに表すための考え方です。

まずは、どの概念がどの役割を担い、どこに業務ルールが置かれているかを意識するだけでも、コードの読み解きやすさが大きく変わります。今回のポイントを整理しましょう。

  • DDD の狙い:業務の意味とルールをコードに宿す
  • 値オブジェクト:値そのものに意味を持たせる
  • エンティティ:ID を持つ対象を表す
  • 集約:まとめて整合性を守る範囲を決める
  • ドメインサービス:複数対象にまたがるルールを表現する
  • MentorApp での対応:Email、User、Topic、各種 Service に形として現れている

次回は、クリーンアーキテクチャ・DDDの考え方に基づいたC#のソリューション・プロジェクト構成を見ていきます。

プロ太

DDD は、業務の意味をコードに宿すための考え方です。引き続き、C#ソリューション・プロジェクト構成について一緒に学んでいきましょう!

ABOUT ME
プロ太
ソフトウェア開発を楽しく、効率的に行う方法を追求しています。 開発者の視点から技術的課題に向き合い、「純粋な技術的興味に基づく探求」と「実践的な課題解決」という二つの柱を両輪として活動しています。