よんログ

ドメイン駆動設計

ドメイン駆動設計 (domain-driven design, 以下DDD) はソフトウェアの設計手法である。

ドメインとは、ソフトウェアが問題解決しようとする領域のことである。 DDDでは、ドメインを中心とするモデルベースな抽象化によってソフトウェア品質に寄与するという考え方が基になっている。

ソフトウェア品質の要因

ソフトウェア品質の要因はいくつか存在するが、その中でDDDが寄与する5つの要素を列挙する。

  1. 保守性 (maintainability)
  2. 信頼性 (reliability)
  3. 再利用性 (reusability)
  4. テスト容易性 (testability)
  5. 理解可能性 (understandability)

参考: List of system quality attributes - Wikipedia

結合度・凝集度

結合度・凝集度はいずれもソースコード品質を表す指標であり、一般的に低結合・高凝集が好ましいとされている。

結合度

結合度 (coupling) とは、モジュールやクラスなどのソフトウェアコンポーネント群がどれほど保守されやすいように分割・整理されているかの尺度であり、結合度が低いほど好ましい。 (分割・整理されている)

結合の程度

手続き型プログラミングにおける結合は以下のように分類できる。

  • 内容結合 (content coupling) (強結合)
  • 共通結合 (common coupling)
  • 外部結合 (external coupling)
  • 制御結合 (control coupling)
  • スタンプ結合 (stamp coupling)
  • データ結合 (data coupling)
  • メッセージ結合 (message coupling) (弱結合)
  • 無結合 (no coupling)

参考: 結合度 - Wikipedia

凝集度

凝集度 (cohesion) とは、モジュールやクラスなどの1つのソフトウェアコンポーネントの構成物間における責務やデータの関連の強さの尺度である。

凝集の程度

凝集の程度は以下のように分類される。

  • 偶発的凝集 (coincidental cohesion) (低凝集)
  • 論理的凝集 (logical cohesion)
  • 時間的凝集 (temporal cohesion)
  • 手続き的凝集 (procedural cohesion)
  • 通信的凝集 (communicational cohesion)
  • 逐次的凝集 (sequential cohesion)
  • 機能的凝集 (functional cohesion) (高凝集)

参考: 凝集度 - Wikipedia

ドメイン凝集度

DDDでは扱うドメインの関連度による凝集度をドメイン凝集度 (domain cohesion) と定義する。

実装言語の選定

DDDではドメイン制約を静的型で表現することによりドメイン操作の静的安全を設計するため、静的型付け言語で実装することが好ましい。

また、静的型でnullability (値がnullになりうるか) を表現できることが好ましい。 Javaの型はnullabilityを表現することができないため、「nullableな式は必ず java.util.Optional でラップする」などの取り決めをしておくか、Kotlinの使用を検討する。

ドメインモデルの実装

値オブジェクト

値オブジェクト (value object) は属性値によって同一性が判断されるドメインモデルの実装パターンである。

値オブジェクトへの予期しない副作用を防ぐため、値オブジェクトの各属性は不変 (immutable) とする。 これはJavaの final やKotlinの val などで実現できる。

値オブジェクトの等価性は属性値が同じかどうかで判定される。 Kotlinを使用している場合は data class を使うことで実現できる。 JavaでLombokを使用している場合は @EqualsAndHashCode あるいはそれを内包するアノテーションで実現できる。

「値オブジェクト」の定義

「値オブジェクト」という呼称はDDD外の文脈においても「値を包んだオブジェクト」の意で用いられるため、その呼称をDDDの概念で上書きしてしまうのは好ましくない。

子クラスの @EqualsAndHashCode の罠

他クラスを継承する子クラスに対して @EqualsAndHashCode やそれを内包するアノテーション (例: @Value) を使用することは推奨されていない。

もし、やむを得ず子クラスに対して @EqualsAndHashCode を使用する場合は、アノテーションの引数 callSupertrue にセットしない限り、親クラスのフィールドが考慮されない equals(), hashCode() が生成されてしまうことに注意する。

参考: @EqualsAndHashCode - Project Lombok

Primitive Obsession

ドメイン層で扱う値を全て値オブジェクトで表し、型検査によって引数の代入ミスを静的に防止するという考え方がある。 しかし、これは引数の代入ミスのリスクをドメイン層から他層にしわ寄せしているに過ぎない。 (どこかで値オブジェクトに詰める操作が必要になるため)

逆に、状態遷移メソッドをもたない単一の値を値オブジェクトとして扱う典型例として、BtoB領域におけるマルチテナントアプリケーションのテナント識別子がある。 テナント識別子の指定を誤ると、最悪な場合でテナントのデータが他テナントに漏洩する重大インシデントリスクがあるため、テナント識別子を値オブジェクト化して型安全を手に入れることは十分割に合うと考える。

非破壊メソッドの命名規則

値オブジェクトは不変であるため、属性の一部を変更する場合は新しい値オブジェクトに詰め直して返す。 このようなオブジェクトを変更しないメソッドを非破壊的 (non-destructive) メソッドという。

非破壊的メソッドは習慣的に受動態を使って命名することで、開発者が破壊的メソッドと勘違いして戻り値を捨ててしまうことを防ぐ。

  1. 破壊的メソッドの例: updateEmail()
  2. 非破壊的メソッドの例: withEmailUpdated()

エンティティ

エンティティ (entity) は識別子 (DBMSが発番するシーケンス番号やUUIDなど) によって同一性が判断されるドメインモデルの実装パターンである。

エンティティは値オブジェクトと異なり、可変であってもよい。 (無論、不変としてもよい) これはエンティティは属性値が多く値の詰替えが困難であることを前提とした妥協案である。

Builderパターンは使わない

エンティティの生成に必要なパラメータが多い場合は、コードの可読性のためにBuilderパターンを使うことが一般的に推奨されている。

しかし、私はBuilderパターンを使うことを推奨しない。 Builderパターンは値がセットされていなくてもビルドできてしまうため、静的安全でない。 また、Kotlinなどのモダンな言語では名前付きパラメータなどで可読性の問題をクリアできるため、わざわざBuilderパターンを使う理由がない。

ORMからエンティティを生成する

一部のORMはエンティティの生成において、エンティティのデフォルトコンストラクタあるいは全フィールドを引数として受け取るコンストラクタやセッターメソッドを要求する。

これらは、「ドメイン制約を満たすオブジェクトのみを存在させる」というDDDの指針と相容れないため、使用は極力避けるべきである。 デフォルトコンストラクタについては、ORM or レポジトリのみが参照できるような可視性制御を行うか、メソッドの命名 (例: fromRepository) で使用用途を示すべきである。

JavaでLombokを使用している場合、アノテーション @AllArgsConstructor(staticName = "fromRepository") を付与することで、staticな全引数コンストラクタを生成できる。

参考: @NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor - Project Lombok

楽観ロックのバージョン管理

永続化データが楽観ロックを採用している場合は、習慣的にロックバージョンをエンティティにもつ。 ロックバージョンは明らかにドメイン都合のものでないが、これは実装コストとのバランスを考えた妥協策である。

集約

集約 (aggregate) はいくつかのエンティティや値オブジェクトをひとまとめにしたモデルの実装パターンである。 複数の値オブジェクト/エンティティを跨ぐドメイン制約を課すことができ、その制約を満たすことを保証するために必ず単一のトランザクション内でまとめて永続化する。

ドメインオブジェクトの状態遷移メソッド

ドメインオブジェクトの状態遷移メソッドは、必ずドメイン制約を満たすような単位で実装する。 ユースケースに依って不整合を生み出してしまうようなメソッドの公開は避け、操作の前に必要なバリデーションを行う。

無駄なゲッター/セッターを生やさない

Lombokのアノテーション @Value@Data を使うと、エンティティの属性値のゲッター/セッターを簡単に生成することができる。

しかし、これはドメイン制約を満たさないメソッドを外部に公開することになり、「ドメイン制約を満たすオブジェクトのみを存在させる」というDDDの指針に反する。 よって、エンティティクラスの全フィールドのゲッター/セッターを自動生成するこれらのアノテーションの使用は推奨しない。

その他のクラス

ドメインサービス

ドメインサービス (service) はドメイン制約をエンティティ or 値オブジェクト or 集約に実装することが難しい場合に使うドメイン手続きの実装パターンである。

例えば、ユーザーのメールアドレスの重複チェックを行う度に、全ユーザーのメールアドレスをフェッチして集約に格納し集約の中でチェックするとパフォーマンス的に大惨事になる。

原則として値オブジェクト or エンティティ or 集約などのドメインオブジェクトのクラスでドメイン制約を実装し、パフォーマンスなどの都合でドメインオブジェクトのクラスによる実装が難しい場合にのみドメインサービスを使うに留める。

抽象化 vs. パフォーマンス

前述のメールアドレスの重複チェックのように、抽象化はしばしばパフォーマンスとトレードオフの関係にある。

「〜Service」接尾辞

多くのDDD本では、習慣と反してドメインサービスのクラス名には「〜Service」を接尾辞につける必要はないとしている。 これは、クラスがサービスであることを区別する必要性がなく、サービスのクラス名は責務を極力簡潔に表すことが好ましいからである。

また、サービスクラスに「エンティティ名 + Service」のような命名をしてしまうと、クラス名が表す責務範囲が広いゆえに改修毎にサービスクラスが肥大化しがちである。

しかし、私は「〜Service」接尾辞は必要だと考えている。 理由は、ドメインサービスはレポジトリを呼ぶことが多く、開発者にそのことを隠蔽すると、N+1のようなパフォーマンス問題に繋がりうるからである。 (つまり、永続層へのアクセスの完全な透過性を提供することは不可能と考える)

ファクトリ

ファクトリ (factory) は、複雑なロジックを含むエンティティ or 集約の生成メソッドである。

ファクトリがレポジトリやドメインサービスを呼び出さない場合は、生成メソッドはファクトリクラスのstaticなメソッドとして実装する。 ファクトリがレポジトリやドメインサービスなどを呼び出す (DIを必要とする) 場合は、生成メソッドはドメインサービスのメソッドとして実装する。

予期しないインスタンス化を防ぐ

ファクトリクラスのようなstaticなメソッドのみを提供するクラスは、予期せずインスタンス化されることを防ぐことが好ましい。

Kotlinを使用している場合は object を使うことにより実現できる。 JavaでLombokを使用している場合は @NoArgsConstructor(access = AccessLevel.PRIVATE) アノテーションをクラスに付与し、デフォルトコンストラクタの可視性を private にすることで実現できる。

参考: @NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor - Project Lombok

レポジトリ

レポジトリ (repository) は永続化や外部サービスなどの副作用を隠蔽し、アプリケーションのインメモリにあるコレクション or ハッシュマップのように振る舞い、データアクセスへの透過性を提供する。

エンティティの識別子

DBMSが発番するシーケンス番号 (MySQLの AUTO_INCREMENT やPosgreSQLの SERIAL など) を識別子に使用しているエンティティを新規に作成するとき、DBMSがレコード挿入の要求を完了してシーケンス番号を返すまでエンティティの識別子が確定しない (nullableになってしまう) ことになる。 エンティティの識別子がnullableになると、識別子を参照する都度null/non-nullでの分岐が必要になり、操作が煩雑になってしまう。

これを避けるには

  • エンティティの次のシーケンス番号のみを取得するレポジトリメソッドを実装しエンティティのファクトリから呼び出す (トランザクションの失敗により欠番が生じうることに注意, 参考: シーケンス操作関数 - PostgreSQL 8.0.4 文書)
  • UUIDなどアプリケーション側で発行できる識別子を使う

などを検討する。

参考: ID生成大全 - Qiita

TODO

  • ドメイン駆動設計の設計指針
  • 境界付けられたコンテキスト
  • ユビキタス言語
  • アーキテクチャ
  • 例外処理
  • レポジトリのテスト
  • 集約ルート
  • CQRS
  • イベントソーシング
  • マイクロサービスアーキテクチャと分散システムの罠
  • fail-fastの原則とfail-softについて

参考