ドメイン駆動設計(DDD)の基礎単語を野球の例で理解してみた

サムネイル

はじめに

こんにちは。エンジニアのNittaです。

弊社では外部の講師の方を招いて研修を行うなど、ドメイン駆動設計(DDD)を積極的に取り入れています。そこで、先日参加した研修を機にドメイン駆動設計の考え方や基本的な用語を例と共に学習してみました。

理解しやすくするために、自分にとって身近な”野球”を例に考えました。野球の『個人成績管理システム』を題材に、ドメイン駆動設計に関する以下の内容について説明していきます。

  • ドメイン駆動設計とは何か
  • ユビキタス言語とは何か
  • ドメインモデルとは何か
  • ドメインオブジェクトとは何か
  • ドメインオブジェクトの3つの種類 (エンティティ、値オブジェクト、ドメインサービス)

ドメイン駆動設計とは何か

ソフトウェア開発において、開発者がその領域(ドメイン)に関する知識を身につけても、すべての知識が直接役に立つわけではありません。重要なのは、ソフトウェアの価値に直結する知識を見極め、それを正しく設計やコードに落とし込むことです。

この考え方を実践する代表的な開発手法がドメイン駆動設計(DDD)です。DDDではビジネスのルールや仕組みを軸に、専門家とエンジニアが共通語(ユビキタス言語)でモデルを作り、そのモデルを基に設計や実装を進めます。これにより複雑な業務ロジックを整理し、将来の変更にも柔軟に対応できるシステムを実現できます。

とはいえ「どの知識がドメインにとって重要で、どれがそうでないのか」を見極めるのは簡単ではありません。ここで、野球の個別成績管理システムで整理してみましょう。

  1. 打率の計算方法:開発に必須

→ 打率は「選手の成績」というドメインロジックに直結しています。DDDの文脈では「打率」という概念はユビキタス言語(※詳しくは後述します)の重要な一部です。

  1. 野球のルール:開発に必須

→ 野球のルールは広範囲ですが「成績をどう記録するか」に影響する部分はドメインモデルに直接関わります。「全ルール」ではなく、成績ドメインに関係するルールのみをモデリング対象とするのがDDD的な判断です。

  1. ベース間の距離:開発にはあまり関係ない

→ ベース間の距離は野球の物理的ルールですが、成績管理のドメインロジックには影響しません。走塁速度を計算するシステムなら「距離」は関係しますが、単純に打率・本塁打・打点などを管理する「成績集計ドメイン」には直接関係しないといえます。

  1. 野球ボールの縫い目の本数 :開発にはあまり関係ない

→ 縫い目の本数は道具の仕様であり、成績の集計やドメインロジックには影響を与えません。

余談ですが…
野球ボールの縫い目が108個なのをご存知ですか?数だけ聞くと煩悩を思わせますが、実際は耐久性や空気抵抗、回転の安定性を追求した結果だそうです。赤い糸はボールをより視認しやすくする工夫とされています。

(参考:野球ボールの縫い目と赤い糸の秘密 – 知られざる108個の縫い目の理由

この例から分かるように、ドメイン知識には「価値のある知識」と「ソフトウェア開発には不要な知識」が混在しています。

利用者にとって価値のあるソフトウェアを作るためには、これらを慎重に切り分け、必要な知識だけを抽出する必要があります。

そして、抽出した知識をチームで正しく共有し、コードに反映させるために使うのが「ユビキタス言語」です。

ユビキタス言語とは

ユビキタス言語とは、チーム全員が同じ言葉で同じ概念を指すためのぶれない共通語のことです。

ユビキタス言語のメリット

  • 認識のズレを防げる
    チーム内で共通の言葉を使うことで、会話・設計・実装が同じ方向を向き、認識の齟齬がなくなる。
  • コードが業務の言葉で書かれる
    コード上でもユビキタス言語がそのまま使われるため、プログラムを読むだけで業務内容が理解しやすくなる。
  • 仕様変更に対応しやすい
    言葉とコードが一致しているため、仕様変更が発生した際に、影響を受ける箇所を素早く把握できる。

ユビキタス言語の例

今回は野球の個別成績管理システムを例に、ユビキタス言語の用語をいくつか作成してみます。

用語意味コード上の表現
打席打者がバッターボックスに入り、アウト・安打・四球などで結果が決まるまでの一連の行為・四球で出塁しても1打席増える
・投手との対戦が完了していない状況で攻守交代になった場合は増えない
PlateAppearance
打数四球・死球・犠打・犠飛の結果を打席からを除いたもの三振、ゴロ、フライなどアウトになってもカウントされるAtBats
打率・安打 ÷ 打数
・小数第3位まで表示
152/500 → .304BattingAverage
規定打席試合数 × 3.1 などリーグ固有の条件のこと143試合ならば443打席QualificationPolicy

このようにユビキタス言語を定義することで、野球にそれほど詳しくない人でも同じ言語で会話できるようになり、認識の齟齬を防ぎながらコードへ正しく知識を埋め込むことができます。

ユビキタス言語を決定する際の注意点

  • 曖昧さを避けて、厳密に決定する
    ユビキタス言語はコード上でもそのまま使われるため、解釈に幅がある曖昧な言葉ではなく、明確で厳密な言葉を選ぶ。
  • ブレインストーミングで重要な概念を洗い出す
    ドメインエキスパート(業務の専門家)と開発者が一緒に議論し、そのビジネス領域で重要となる概念や処理をリストアップする。
  • 頻繁に使う言葉から優先的に定義する
    会話や要件定義で何度も登場する言葉は、メンバー間で解釈が分かれやすいため、まず優先して定義する。
  • 使わない言葉(禁止語)を明確にする
    曖昧で誤解を招く表現は「使ってはいけない言葉」としてリスト化し、代わりに具体的で業務に即した表現を使う。

ユビキタス言語を決定することで、チーム全員が同じ言葉で会話できるようになります。 次はその言葉をどうやってソフトウェアの設計に反映させるかですが、ここで登場するのが「ドメインモデル」です。

ドメインモデルとは

ソフトウェアは、現実のすべての情報を細かく再現する必要はなく、「そのソフトウェアにとって大事な部分」だけを抜き出して表現する必要があります。

その「大事な部分を整理して抽象化したもの」をドメインモデルと呼びます。

ボールの例

  • 野球選手にとってのボール:「投げる」「打つ」「捕る」といったプレイに使う道具
    → 重要なのは「縫い目」「重さ」「反発力」など
  • スポーツ用品店にとってのボール:「販売する商品」
    → 重要なのは「仕入れ価格」「在庫数」「JANコード」など

同じ「ボール」でも、立場が違えば重視する性質はまったく異なります。

つまり何を抜き出して、何を捨てるかは、そのドメインの目的次第となります。

ドメインオブジェクト

ここまでで 「ドメインモデルはソフトウェアに必要な知識を抽象化したもの」と説明してきましたが、モデルはあくまで考え方・図・用語の整理です。

ソフトウェアとして動くためには、それをコード上のモジュールとして表現する必要があり、ここで登場するのが「ドメインオブジェクト」です。

ドメインオブジェクトとは、ドメインモデルをプログラムで動かせるようにしたもののことです。

実際の例:打率

たとえば「打率(BattingAverage)」というドメインモデルがあったとします。

class BattingAverage {

  constructor(private hits: number, private atBats: number) {}

  calculate(): number {

    if (this.atBats === 0) return 0;

    return this.hits / this.atBats;

  }

  displayFormat(): string {

    return this.calculate().toFixed(3);

  }

  isQualified(minimumAtBats: number): boolean {

    return this.atBats >= minimumAtBats;

  }

}

【特徴】

  • 「打率」という概念を1つのまとまりとして扱える
  • 関連するロジックを一箇所にまとめられる
  • 将来的にも拡張性が高い
  • ビジネスの重要な概念をコードで明確に表現できる

このようにクラスでドメインオブジェクトを定義することで、ドメインモデルをそのままコードとして動かすことができます。

どうしてクラスで表現するのか?関数ではできないのか?

DDDの記事について調べていると、どのコード例もクラスを使用したものばかりでした。

ただ「関数でも同じことができるのでは?」とずっと疑問で、どうしてクラスを使うのか不明だったので、関数でも上と同様に打率のドメインオブジェクトを定義してみます。

関数で表現する場合

type BattingAverageData = {

  hits: number;

  atBats: number;

}

function calculateBattingAverage(data: BattingAverageData): number {

  if (data.atBats === 0) return 0;

  return data.hits / data.atBats;

}

function formatBattingAverage(data: BattingAverageData): string {

  return calculateBattingAverage(data).toFixed(3);

}

function isQualifiedBatter(data: BattingAverageData, minimumAtBats: number): boolean {

  return data.atBats >= minimumAtBats;

}

実際、関数でも同じように定義できました。しかし、クラスで定義するよりも個々の処理がバラバラになったように感じました。

高凝集・低結合の視点から見ると

ドメイン駆動設計自体は「ドメイン知識を中心にモデル化する」開発手法ですが、そのモデルを保守しやすく、変更に強い構造にするために、高凝集・低結合の考え方を積極的に取り入れています。

  • 高凝集:ドメインモデル(エンティティや値オブジェクト)が関連する振る舞いを一箇所にまとめる。
  • 低結合:リポジトリやサービスを介してインフラ層に依存しすぎないようにする。

高凝集・低結合の観点から考えると、クラスの方が適している理由が見えてきます。

クラスの場合

  • 打率に関する全ての責任(計算・表示・検証)が1つのクラスにまとまっている
  • 関連する処理が近くに配置され、変更時の影響範囲が明確
  • 「打率」という概念の完全性を保ちやすい

■関数の場合

  • 関連する関数がコード上でバラバラに配置される可能性
  • どの関数が「打率」に関連するのかが分かりにくくなる

つまり、DDDでクラスがよく使われる理由の一つは、ドメインの重要な概念を高凝集で表現できるからだと感じました。

ドメインオブジェクトの種類

ここまで打率を例にドメインオブジェクトを学んできましたが、DDDではドメインオブジェクトを以下の3つに分類することが多いようです。

  1. エンティティ
  2. 値オブジェクト
  3. ドメインサービス

1. エンティティ

エンティティの特徴

  • 一意な識別子を持つ
  • 値は可変で良い

エンティティで定義するものの例として野球で例えるなら選手データが挙げられます。

背番号(識別子)名前打率
1山田 太郎0.304
3山田 太郎0.156
5田中 三郎0.375 → 0.400

上のように選手というエンティティについて考えます

  • たまたま山田太郎さんがチーム内に2人いる
    →背番号という一意な識別子によって判定されるので、別人であると判定される
  • 田中三郎さんの打率が0.375から0.400に上がった
    →たとえ打率が変わっても、彼が田中三郎さんであることは変わりない

これら2つがエンティティの特徴です。

2. 値オブジェクト

値オブジェクトの特徴

  • 識別子を持たない
  • 値は不変でなければならない

ここで2つの野球ボールについて考えてみました。

重さ縫い目の本数
ボールA148g108本
ボールB148g108本

この場合、AとBは同じボールと考えられます。なぜならボールには識別子はなく、属性値も同じであるためです。

また、一度作成されたボールの縫い目を変えることや重さが変わることはありません。

値オブジェクトがエンティティになる場合

仮に先ほどの条件を満たすボールでも、エンティティに分類する場合があります。

それは以下のような例です。

  • 一流選手の今期50本目のホームランボール

この場合、シーズン本塁打IDという識別子で50という値を管理する必要があり、この瞬間に特別なボールになります。

このように、同じ「もの」でも、ドメインにおける重要性によって分類が変わることがDDDでは起こりうるそうです。

3. ドメインサービス

ドメインサービスとは、エンティティにも値オブジェクトにも属さない、ビジネスロジックのことです。

イマイチ掴めなかったので「選手の成績ランキングサービス」を例に考えてみました。

リーグ内成績ランキングサービス

  • 打率やホームラン数などで選手を降順に並び替える
  • 打率は打数が少ないと外れ値が出る可能性があるので最低50打席立った選手のみを対象
  • 打率が同じ場合は打席数が多い選手が上位になる

上のようなサービスがあったとして、このサービスはエンティティや値オブジェクトに属するでしょうか?

  • 1人の選手(Player)に属するか
    → 1人の選手が他の選手全員を評価するのはおかしい
  • 1つのチーム(Team)に属するか
    → リーグ全体のランキングなので適さない
  • 1つのリーグ(League)に属するか
    → リーグは組織であり、ランキングを決める主体ではない

どのオブジェクトに入れようとしても違和感があります。 この様などのオブジェクトにも属さない重要なビジネスロジックがドメインサービスです。

まとめ

今回は、野球の個人成績管理システムを例に、ドメイン駆動設計(DDD)の基本的な考え方や用語を解説しました。

  • ドメイン駆動設計は、システムの価値を最大化するために、ビジネスの本質的な知識をコードに正確に落とし込むための開発手法。
  • ユビキタス言語は、チーム全員が同じ言葉で話すための共通語。この言語が、会話・設計・コードを統一し、認識のズレを防ぐ。
  • ドメインモデルは、システムにとって重要な概念を抽象化したもの。このモデルをコードで動かせるようにしたものがドメインオブジェクト
  • ドメインオブジェクトは、エンティティ(識別子を持つ)、値オブジェクト(識別子を持たず不変)、ドメインサービス(どちらにも属さないビジネスロジック)の3つに分類される。

今後はリポジトリ、集約、ファクトリ、ドメインサービスなどの用語も同様に整理した後に、具体的なコードを絡めて理解していきたいです。

ABOUT US
Koki Nitta
25年新卒で株式会社アイスリーデザインに入社。 入社後はReact, Next, Laraval等を使用したプロジェクトに携わる。