ドメイン駆動設計とは? その本質と初めからをゼロから解説

ペンと一緒に置かれた、手書きのアプリ設計メモ。

みなさまこんにちは、社内でアプリ開発を行っているおさかなです。 プロジェクトで開発しているときに「要件が複雑になってきた」「仕様変更が入るたびにコードが壊れそう」と感じることはないでしょうか? 私自身これまで機能追加を行う際は実装スピードを優先していたこともあり、徐々にシステムがいびつな構成になって新規設計のたびに頭を抱えるということがありました。

しかし、ソフトウェア開発がスピードや機能だけでなく、業務そのものを正しく理解して再現することに重きを置かれるようになった今、「ドメイン駆動設計(DDD)」という設計手法が注目されています。DDDはエンジニアとビジネスの距離を縮めてくれ、複雑な業務をシンプルに捉えるための武器になります。 私は業務の中でDDDについて学ぶ機会があり、その開発手法を現在の開発に取り入れることでシステムの有用性を実感することができたので、この記事では、DDDの基本についてわかりやすく紹介していきたいと思います。

ドメイン駆動設計とは?

システム開発の現場では、「仕様変更に弱い」「業務の本質がコードに反映されていない」といった悩みがよく聞かれます。こうした課題に対処するために生まれたのが、「ドメイン駆動設計(DDD: Domain-Driven Design)」です。DDDの特徴は、業務(ドメイン)を深く理解し、それをソフトウェア設計の中心に据えるという考え方です。

DDDでは、まず「ドメイン」という言葉を明確に定義します。これは、システムが解決しようとしているビジネス上の問題領域(プログラムを適用する対象となる領域)のことを指します。たとえば、ECサイトであれば「商品」「顧客」「注文」「在庫」などがドメインにあたります。「ドメインが何か」ということよりも「ドメインには何が含まれるか」を考えることが重要です。

DDDの大きなポイントは、業務に登場する用語やルールを、ソフトウェアの中にもそのまま取り込むことです。これを実現するために、開発者と業務担当者が協力しながら、「ユビキタス言語(Ubiquitous Language)」という共通の言葉づかいを決め、それを設計・実装にも一貫して使います。たとえば、業務で「注文」という概念があれば、コードの中でも「Order」という名前のクラスを作り、そのまま「注文」に関する処理を持たせます。これによって、コードを読めば業務の流れがわかるようになります。 DDDのイメージを簡単な図で示すと以下のようになります。

ドメイン駆動設計のイメージ
ドメイン駆動設計のイメージ

このように、現実の業務の動きを、そのままプログラムの構造に落とし込むのがDDDの特徴です。これにより、業務の変更にも柔軟に対応できる設計が可能になります。

また、DDDでは「エンティティ(Entity)」「値オブジェクト(Value Object)」「集約(Aggregate)」「リポジトリ(Repository)」といった、設計を整理するための道具も用意されています。これらをうまく活用することで、複雑な業務ロジックも整理しやすくなります。つまりDDDとは、「難しい設計手法」ではなく、「業務をきちんと理解し、それを反映したシステムを作るための考え方」と表現することができます。

用語解説:ドメイン駆動設計のキーワード

ドメイン駆動設計を理解するうえで、いくつかの専門用語が登場します。最初はとっつきにくく感じるかもしれませんが、一つひとつはとても分かりやすい考え方です。この章では、DDDの設計に欠かせない基本的な概念を解説します。

ドメイン駆動設計の要素
ドメイン駆動設計の要素

  • エンティティ
    「識別できるもの」をエンティティといいます。たとえばECサイトの顧客を表す場合、名前が同じでもIDが違えば別人として扱う必要があります。こうした、「一意に識別できる存在」をエンティティと呼びます。
    例)顧客(Customer)、注文(Order)、社員(Employee)など

  • 値オブジェクト
    「識別よりも値が大事なもの」を値オブジェクトといいます。 たとえばECサイトに登録された「お届け先の住所」などは、同じ内容ならどれも同じ意味になります。そうした、変更されない“値そのもの”を表すのが値オブジェクトです。
    例:住所(Address)、金額(Money)、日付範囲(DateRange)など

  • ドメインサービス
    エンティティや値オブジェクトに収まらない処理を扱う場所です。 「複数のオブジェクトにまたがって行う操作」など、役割が分散しすぎる場合はドメインサービスで扱います。

  • リポジトリ
    「エンティティを保存・取得する場所」のことをリポジトリといいます。 データベースにアクセスして情報を保存・検索する仕組みを、コード上ではリポジトリという形で表現します。
    例:OrderRepository → 注文を保存したり検索したりする役割

  • アプリケーションサービス
    ユーザーの操作や外部とのやり取りをコントロールする場所です。 ドメインのロジックそのものではなく、「何をいつ呼び出すか」を司るレイヤーです。

ドメイン駆動設計の進め方

DDDを実践する際は、いきなりコーディングを始めるのではなく、「業務(ドメイン)を深く理解するところからスタートする」ことが大きな特徴です。ドメインとは、そのシステムが解決しようとしている問題領域のことで、DDDではこのドメインの知識に基づいた設計を重視します。DDDを進める一般的な流れは、次のようなステップになります。

  1. ドメインの理解とユビキタス言語の定義
    最初に、エンジニアとドメインエキスパート(現場の業務担当者など)が密にコミュニケーションを取りながら、業務の流れや課題を明確にしていきます。このとき重要なのが「ユビキタス言語」です。これは、エンジニアとドメインエキスパートの間で共通に使う業務用語のことで、コードにもこの言葉をそのまま使うことで、コミュニケーションミスや仕様の誤解を減らします。

    ユビキタス言語
    ユビキタス言語
    この工程の注意事項として、エンジニアがシステムに近い専門用語を使うほどドメインエキスパートはシステムの理解を放棄してしまいがちです。エンジニアはドメインエキスパートの所属している世界を理解するために共通の言葉でコミュニケーションすることが大切です。また、ユビキタス言語はプロジェクトのために作られる共通言語であって、ドメインエキスパートの言葉をそのまま使用するわけではないという点も頭に入れておく必要があります。

  2. モデルの検討
    ドメインを理解できたら、それをコードとして表現する「モデル」を設計します。モデルとは「現実の事象や概念を抽象化して定義したもの」のことで、もう少し分かりやすい言い方をすると「ドメインのふるまいや状態を表すもの」です。たとえば、ECサイトであれば「注文」「商品」「在庫」などがモデルになります。DDDでは、このモデルをオブジェクト指向の考え方で設計し、それぞれに意味のあるメソッド(ふるまい)を持たせることで、ビジネスロジックを分かりやすく整理します。必要に応じてパッケージ図や業務フロー図などを使用することで全体を俯瞰することができ、より良い設計に近づけることができます。

  3. 実装と継続的なリファクタリング
    モデルが決まったら、それをコードとして実装していきます。このとき、ドメインの概念を反映した「エンティティ」「値オブジェクト」「ドメインサービス」などの要素に分けて実装していきます。DDDでは、仕様の変化やドメインの理解に応じて、モデルを見直し、コードをリファクタリングすることも前提としています。 例えば、注文をモデル化したコードは以下のように書くことができます。

//エンティティの例
public class Order
{
    public Guid Id { get; }
    public DateTime OrderDate { get; }
    public bool IsCanceled { get; private set; }
    public Money GoodsValue {get;}

    public Order(Guid id, DateTime orderDate, Money goodsValue)
    {
        Id = id;
        OrderDate = orderDate;
        IsCanceled = false;
        GoodsValue = goodsValue;
    }

    public void Cancel(DateTime currentDate)
    {
        if ((currentDate - OrderDate).TotalDays > 3)
        {
            throw new InvalidOperationException("キャンセル期限を過ぎています。");
        }

        IsCanceled = true;
    }
}

この例では、「3日以内でないとキャンセルできない」という業務ルールを、Cancel() メソッドで表現しています。また、値オブジェクトのコード例は以下のようになります。

//金額を値オブジェクトで実装した例
public sealed class Money : IEquatable<Money>
{
    public decimal Amount { get; }

    public Money(decimal amount)
    {
        if (amount < 0)
            throw new ArgumentException("金額は0以上である必要があります。");

        Amount = decimal.Round(amount, 2); // 小数点以下2桁で固定
    }

    public Money Add(Money other)
    {
        return new Money(this.Amount + other.Amount);
    }

    public Money Subtract(Money other)
    {
        var result = this.Amount - other.Amount;
        if (result < 0)
            throw new InvalidOperationException("結果がマイナスになる金額の減算はできません。");

        return new Money(result);
    }

    public bool IsGreaterThan(Money other) => this.Amount > other.Amount;
    public bool IsLessThan(Money other) => this.Amount < other.Amount;

    public override string ToString() => $"{Amount:C}";

    public override bool Equals(object obj) => Equals(obj as Money);

    public bool Equals(Money other)
    {
        return other != null && Amount == other.Amount;
    }

    public override int GetHashCode() => Amount.GetHashCode();
}

値オブジェクトを作成することでコードの意味を明確にすることができます。また、加算や減算、バリデーションなどを値オブジェクト内に閉じ込めることができるので再利用しやすいといったメリットがあります。

これらのエンティティや値オブジェクトのモデル化がドメイン駆動設計の中核となります。このように、実際の業務を適切にモデル化し、それをエンティティや値オブジェクトなどの実装に落とし込むことで業務ロジックに沿ったシンプルな設計を実現することができるようになります。

Tips① ドメインロジックを使用するときはアプリケーションサービスを実装する
ビジネス処理(ユースケース)を実装したいときは「アプリケーションサービス」で行います。アプリケーションサービスは、ドメインモデルに処理を委譲し、永続化も行います。
※「永続化」とはドメインモデルの状態をデータベースなどに保存して、プログラムを終了してもあとで再利用可能にすることを指します。
public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }

    public void CancelOrder(Guid orderId)
    {
        var order = _repository.FindById(orderId);
        if (order == null) throw new Exception("注文が見つかりません。");

        order.Cancel(DateTime.Now);

        _repository.Save(order);
    }
}
Tips② 永続化するときは「リポジトリ」で抽象化する
データベースとやりとりするときは、凝集性の観点でドメインとは分離することが良いとされています。インターフェースなどを使用して技術的詳細から切り離すことで、柔軟で保守・テストがしやすい設計を実現できます。
public interface IOrderRepository
{
    Order? FindById(Guid id);
    void Save(Order order);
}
Tips③ 境界づけられたコンテキストの明確化
ドメインが広範で複雑な場合、すべてを一つのモデルで表現するのは困難です。そこで登場するのが「境界づけられたコンテキスト(Bounded Context)」という考え方です。これは、モデルやユビキタス言語が通用する範囲を定義するものです。たとえば、販売と在庫では「商品」という言葉の意味が微妙に異なる場合があるため、それぞれに別のコンテキストを持たせることで、混乱を避けます。 同じクラス名にして、全く別の名前として定義することも可能です。
namespace Inventory.Mode.Product
{
    // 在庫管理のコンテキスト
    public class Product
    {
        public string SKU { get; set; }
        public int Quantity { get; set; }
    }
}

namespace Sales.Model.Product
{
    // 販売のコンテキスト
    public class Product
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

現場でのドメイン駆動設計思想の導入

ここまでドメイン駆動設計に関して述べてきましたが、業務で実際に私がドメイン駆動設計を取り入れて開発した例とその感想を簡単にご紹介します。 仕事でカメラを制御するアプリを開発しているのですが、その中で「カメラの各種パラメータを設定する機能」を設計・実装することがありました。このとき、次のような課題や要件がありました。

  • パラメータごとに値のチェック(バリデーション)が必要
  • 同じパラメータを別の機器でも使いまわしたい
  • アジャイル開発なので、仕様変更が頻繁に発生する
  • パラメータが増えるにつれてコードが複雑になりやすい

こうした背景から、「メンテナンス性の高い柔軟な設計」を目指して、ドメイン駆動設計(DDD)を取り入れることにしました。具体的には、各パラメータを「値オブジェクト」として定義し、それらを持つ「カメラ」クラスをドメインモデルとして設計しました(下図参照)。

簡易的なクラス図
簡易的なクラス図

値オブジェクトには、それぞれの有効性チェック(バリデーション)を責任として持たせており、ドメインモデル側ではそれらを組み合わせて構成しています。これにより、クラスの役割が明確になってコードの見通しも良くなり、保守もしやすくなりました。 また、別の機器(たとえばSwitcher)でも同じようなパラメータが必要だったのですが、値オブジェクトを再利用するだけで簡単に対応できたのも大きなメリットでした。 最近では、カメラの種類やパラメータが増えてきたこともあり、クラスを生成するためのファクトリークラスを導入したり、役割ごとにフォルダ構成を整理したりとリファクタリングも進めています。DDDに沿って設計することでコードも整理され、実装工数の見積もりも正確になっていることを実感しており、今後の設計でも積極的に使用していきたいと感じています。

ドメイン駆動設計のメリット・デメリット

前節で実体験について述べましたが、一般的にドメイン駆動設計によってシステムを開発することで下記のようなメリットとデメリットがあると言われています。

メリット

  • 業務知識がコードに反映される
    ドメインモデルを中心に設計することで、業務ルールがソースコードに自然に組み込まれ、ビジネス要件の理解と保守がしやすくなります。
  • 変更に強くなる
    ドメインとアプリケーションの責務を分離し、依存関係を整理することで、機能追加や仕様変更が局所的に行える構造になり、柔軟性が向上します。
  • 関係者との認識のずれが減る
    ユビキタス言語により開発者と業務担当者が共通の言葉で会話するため、認識の不一致や仕様の齟齬が減少します。

    デメリット

  • 学習コストが高い
    エンティティ、値オブジェクト、リポジトリ、集約などの概念を理解し、適切に使いこなすには一定の設計スキルと経験が必要です。
  • 初期設計に時間がかかる
    ドメイン理解やユビキタス言語の確立に時間を要するため、プロジェクトの立ち上げ段階ではスピードが遅くなる傾向があります。
  • 小規模開発では過剰設計になる可能性がある
    シンプルな要件に対してもアーキテクチャが複雑化しがちで、DDDの恩恵が十分に得られない場合があります。

まとめ

今回はDDDの基本的な考え方について説明しました。DDDは、技術的な実装よりもまず「業務の本質」に焦点を当て、複雑なビジネス要件を整理・解決していくための強力なアプローチです。設計思想のすべてを取り入れる必要はありませんが、リポジトリの抽象化やドメインモデルの明確化といった小さなことから始めることで、コードの見通しやチーム内の共通理解が大きく向上させることができます。

ビジネスと技術の橋渡しをする設計手法として、DDDは多くの開発現場で有効な考え方です。DDDは非常に奥の深い分野ですので、私自身まだまだ学習中であり本稿で紹介した内容はほんの触りにすぎません。興味を持たれた方はぜひご自分で調べてより良い設計のヒントを模索されるのも良いのではないでしょうか。では、また次の記事でお会いしましょう!