Skip to content

ドメインサービス完全ガイド:DDD実装の実践手法

はじめに

ドメイン駆動設計(DDD)において、値オブジェクトやエンティティは重要な構成要素です。しかし、システム開発を進める中で、これらのオブジェクトに記述すると不自然に感じられる処理に遭遇することがあります。例えば、複数のオブジェクトを横断する処理や、特定のオブジェクトに属さないビジネスロジックなどが該当します。

本記事では、このような不自然さを解消するドメインサービスについて解説します。ドメインサービスの基本概念から具体的な実装手法まで、実際のコード例を交えながらご紹介します。

まずドメインサービスの基本概念を理解し、次に実装における注意点、最後に具体的な実践例について詳しくご説明します。

ドメインサービスの基本概念

ドメインサービスとは何か

ドメインサービスは、値オブジェクトやエンティティなどのドメインオブジェクトに記述すると不自然になる振る舞いを解決するためのオブジェクトです。ソフトウェアシステムにおいて、ドメインのビジネスロジックを適切に表現することが重要ですが、すべての処理を値オブジェクトやエンティティに配置できるわけではありません。

ドメインサービスは、こうした「どこに記述すべきか判断が難しい振る舞い」を適切に扱うための仕組みを提供します。重要なのは、ドメインサービスがあくまでドメイン層に属するオブジェクトであり、ドメインのルールや概念を表現するために存在するという点です。

値オブジェクトやエンティティとの違い

値オブジェクトやエンティティは、自身の状態と振る舞いをカプセル化するオブジェクトです。一方、ドメインサービスは状態を持たず、純粋に処理を実行する役割に特化しています。

ユーザー名の文字数や使用可能な文字の制限は、その値だけで正否を判断できるため、値オブジェクトに持たせるべきです。一方、ユーザー名の重複可否は他のユーザーとの比較を必要とし、単一のオブジェクトでは完結しないため、個々のユーザーオブジェクトが知るべきルールではありません。

特定のオブジェクトに帰属させることが不自然な振る舞いが、ドメインサービスの対象となります。

ドメインサービスが必要となる場面

ドメインサービスが必要となる代表的な場面は、以下のような状況です。

まず、複数のエンティティや値オブジェクトを組み合わせた処理を行う必要がある場合です。単一のオブジェクトでは完結せず、複数のオブジェクトの協調が必要な処理は、ドメインサービスに記述することで責務が明確になります。

次に、ドメインの概念を表現するが特定のオブジェクトに属さない処理です。ビジネスルールとして重要であるにもかかわらず、エンティティや値オブジェクトのどちらにも自然に配置できない処理が該当します。

最後に、データストアとの連携が必要な不変条件の確認です。オブジェクトの生成や更新時に、既存のデータとの整合性を確認する必要がある場合、ドメインサービスを介して検証を行うことができます。

不自然なふるまいの解決手法

値オブジェクトやエンティティに記述すべきでない処理

オブジェクト指向設計において、各オブジェクトは自身に関連する処理をカプセル化することが基本原則です。しかし、すべての処理をこの原則に従って配置しようとすると、かえってコードが不自然になる場合があります。

例えば、ユーザーエンティティにユーザーの重複確認メソッドを追加すると、そのメソッドは他のすべてのユーザーの情報にアクセスする必要があります。これは、単一のユーザーオブジェクトが本来持つべき責務を超えています。同様に、データストアへの直接的なアクセスをエンティティに記述することも、ドメインモデルとインフラストラクチャ層の境界を曖昧にしてしまいます。

こうした処理は、その性質上、特定のオブジェクトに属させるべきではありません。むしろ、独立したサービスとして定義することで、コードの意図が明確になります。

ドメインサービスによる適切な責務の分離

ドメインサービスを導入することで、値オブジェクトやエンティティに記述すると不自然になる振る舞いを適切に分離できます。重要なのは、ドメインサービスに記述するのは「不自然なふるまい」に限定するということです。

値オブジェクトやエンティティで自然に表現できる処理まで、すべてドメインサービスに移譲してしまうと、ドメインモデルが貧弱になってしまいます。ドメインオブジェクトはゲッターとセッターだけを持つデータの入れ物となり、本来記述されるべき知識やルールが失われます。

適切な設計では、まず値オブジェクトやエンティティに記述できるかを検討し、それが不自然である場合にのみドメインサービスを使用します。この判断基準を明確に持つことが、保守性の高いコードを実現する鍵となります。

オブジェクト指向設計との整合性

ドメインサービスの導入は、オブジェクト指向設計の原則と矛盾するものではありません。むしろ、データとふるまいを適切に組み合わせるという基本原則を守るための手段です。

ドメインオブジェクトに本来記述されるべき知識やルールを保持させながら、複数のオブジェクトにまたがる処理や特定のオブジェクトに帰属させることが困難な処理をドメインサービスに委譲します。これにより、各オブジェクトは適切な責務を持ち、システム全体の設計が整合性を保ちます。

重要なのは、ドメインサービスを「ただのデータ処理を行う無国籍なオブジェクト」ではなく、「ドメインの概念を表現するための手段」として位置づけることです。ドメインサービスもまた、ドメインモデルの一部であり、ビジネスロジックを表現する役割を担っています。

ドメインサービス実装の実践例

ユーザー重複確認機能の実装

ユーザー重複確認は、ドメインサービスの典型的な活用例です。ここでは、ユーザー名の重複を許可しないというビジネスルールを実装する方法をご紹介します。

まず、ユーザーを表現するエンティティを定義します。ユーザーは識別子であるユーザーIDと、ユーザー名というプロパティを持ちます。


class User(
    val id: UserId,
    name: UserName
) {
    var name: UserName = name
        private set

    init {
        requireNotNull(id) { "id cannot be null" }
        requireNotNull(name) { "name cannot be null" }
    }

    fun changeName(name: UserName) {
        requireNotNull(name) { "name cannot be null" }
        this.name = name
    }
}

次に、ユーザーIDとユーザー名を表す値オブジェクトを定義します。これらは単純なラッパーですが、ドメインの概念を型として表現する重要な役割を果たします。


data class UserId(val value: String) {
    init {
        requireNotNull(value) { "value cannot be null" }
    }
}

data class UserName(val value: String) {
    init {
        requireNotNull(value) { "value cannot be null" }
        require(value.length >= 3) { "ユーザー名は3文字以上です" }
    }
}

ここで重要なのは、ユーザー名の文字数制限といった個別のルールはUserName値オブジェクトに記述されているという点です。しかし、システム全体での重複確認は、UserやUserNameクラスに記述することが適切ではありません。

そこで、ユーザーの重複を確認するドメインサービスを実装します。


class UserService {
    fun exists(user: User): Boolean {
        // 重複を確認する処理
        // データストアにアクセスして確認を行う
        return false // プレースホルダー
    }
}

このドメインサービスを利用することで、ユーザー作成処理において重複確認を行うことができます。


class Program {
    fun createUser(userName: String) {
        val user = User(
            UserId(java.util.UUID.randomUUID().toString()),
            UserName(userName)
        )

        val userService = UserService()
        if (userService.exists(user)) {
            throw Exception("${userName}は既に存在しています")
        }

        // ユーザーを保存する処理
    }
}

このように実装することで、重複確認というドメインのルールを適切な場所に配置しながら、各オブジェクトの責務を明確に保つことができます。

物流システムにおける輸送処理の実装

次に、より複雑な例として物流システムにおける輸送処理を見てみましょう。物流システムでは、荷物を拠点から直接配送先に届けるのではなく、複数の拠点を経由して配送します。

まず、物流拠点というエンティティを定義します。物流拠点には出庫と入庫という振る舞いがあります。


class PhysicalDistributionBase {
    fun ship(baggage: Baggage): Baggage {
        // 出庫処理
        return baggage // プレースホルダー
    }

    fun receive(baggage: Baggage) {
        // 入庫処理
    }
}

拠点から拠点への荷物の輸送は、出庫と入庫がセットで実行される必要があります。誤って出庫だけを行い、入庫を忘れてしまうと、荷物が行方不明になってしまいます。

このような複数のオブジェクトにまたがる処理を一つのまとまりとして表現するために、輸送ドメインサービスを定義します。


class TransportService {
    fun transport(from: PhysicalDistributionBase, to: PhysicalDistributionBase, baggage: Baggage) {
        val shippedBaggage = from.ship(baggage)
        to.receive(shippedBaggage)

        // 配送の記録を行う処理など
    }
}

このドメインサービスを使用することで、輸送という概念を明示的にコードで表現できます。また、出庫と入庫が必ずセットで実行されることが保証され、ビジネスルールの整合性が維持されます。

物流拠点が他の拠点へ直接荷物を配送するメソッドを持つという設計も考えられますが、それは拠点の責務を超えています。輸送という概念を独立したドメインサービスとして切り出すことで、ドメインモデルの構造がより自然になります。

ドメインサービスとアプリケーションサービスの使い分け

ドメインサービスを理解する上で重要なのが、アプリケーションサービスとの違いです。両者は名前に「サービス」という言葉を含んでいますが、その役割は大きく異なります。

ドメインサービスは、ドメインのビジネスロジックを表現するためのオブジェクトです。値オブジェクトやエンティティと同様に、ドメイン層に属します。一方、アプリケーションサービスは、ユースケースを実現するための調整役であり、アプリケーション層に属します。

具体的には、アプリケーションサービスは以下のような役割を担います。

  • リポジトリを使用してエンティティを取得する
  • ドメインサービスやエンティティのメソッドを呼び出す
  • トランザクションを管理する
  • 処理結果をクライアントに返す

重要なのは、アプリケーションサービス自体にはビジネスロジックを記述しないという点です。ビジネスロジックはあくまでドメイン層に配置され、アプリケーションサービスはそれらを適切に組み合わせてユースケースを実現します。

この区別を明確にすることで、ドメインロジックの再利用性が高まり、テストも容易になります。

ドメインサービス設計の注意点

ドメインサービスの濫用を避ける

ドメインサービスは便利な仕組みですが、濫用には注意が必要です。すべての処理をドメインサービスに記述してしまうと、エンティティや値オブジェクトがゲッターとセッターだけを持つ貧弱なオブジェクトになってしまいます。

これは、オブジェクト指向設計におけるデータとふるまいの結合という基本原則に反します。ドメインモデルが本来持つべき知識やルールが失われ、結果としてコードの保守性が低下します。

ドメインサービスは、エンティティや値オブジェクトに記述すると不自然になる処理に限定して使用してください。まずは「この処理はどのオブジェクトに属するか」を検討し、適切なオブジェクトが見つからない場合にのみドメインサービスを選択します。

可能な限りドメインサービスの使用を避けることが、健全なドメインモデルを維持する秘訣です。

適切な命名規則とパターン

ドメインサービスの命名には、いくつかのパターンがあります。適切な命名規則を採用することで、コードの意図が明確になります。

最も基本的なパターンは「ドメインの概念 + Service」という形式です。例えば、ユーザーに関するドメインサービスであればUserServiceとなります。この命名規則は、ドメインサービスがどのドメイン概念に関連しているかを明示します。

もう一つのパターンは、ドメインの概念とServiceの組み合わせではなく、処理の内容を直接表現する形式です。例えば、DuplicateCheckServiceのように、具体的な処理内容を名前に含めます。

どちらのパターンを採用するかは、プロジェクトの規約やチームの合意によって決定します。重要なのは、一貫性を保つことです。同じプロジェクト内で複数の命名パターンが混在すると、コードの可読性が低下します。

また、ドメインサービスの名前は、ドメイン専門家との会話で使用される用語と一致させることが理想的です。これにより、コードとビジネスの要件が直接的に対応し、理解しやすくなります。

データストアとの関係性

ドメインサービスとデータストアの関係について、明確な方針を持つことが重要です。ドメインサービスは、原則としてデータストアに直接アクセスすべきではありません。

データストアへのアクセスは、リポジトリパターンを通じて行うことが推奨されます。リポジトリはインフラストラクチャ層に属し、データの永続化に関する詳細を隠蔽します。ドメインサービスは、リポジトリを通じて必要なデータを取得し、ドメインロジックを実行します。

例えば、ユーザー重複確認のドメインサービスは、UserRepositoryを使用してデータストアからユーザー情報を取得します。ドメインサービス自体は、データがリレーショナルデータベースに保存されているのか、NoSQLデータベースなのか、あるいはメモリ上のデータ構造なのかを知る必要がありません。

この分離により、データストアの実装を変更してもドメインサービスのコードに影響を与えずに済みます。また、テスト時にはモックのリポジトリを使用することで、データストアなしでドメインロジックを検証できます。

ただし、特定のプロジェクトの制約や実装の複雑性によっては、例外的な対応が必要になる場合もあります。その場合でも、原則を理解した上で意図的に判断することが大切です。

終わりに

本記事では、ドメイン駆動設計におけるドメインサービスについて解説しました。値オブジェクトやエンティティに記述すると不自然になる振る舞いは、ドメインサービスとして切り出すことで適切に表現できます。ユーザー重複確認や物流システムの輸送処理といった具体例を通じて、実装手法をご紹介しました。

これらの手法を活用することで、ドメインモデルの整合性を保ちながら、保守性の高いコードを実現することが期待できます。まずは小さな機能からドメインサービスの導入を試みることをお勧めします。

ドメインサービスは便利な仕組みですが、濫用は避けることが重要です。まずはエンティティや値オブジェクトへの配置を検討し、それが不自然である場合にのみドメインサービスを選択してください。適切な判断基準を持つことで、長期的に価値のあるソフトウェアを構築できます。

技術の進化は続きますが、本記事で紹介した基本的な考え方は長期的に有効です。ぜひ実際のプロジェクトで試してみてください。

コメントを残す