📑

業務概念を型として表現する

に公開

はじめに

ドメインモデリングという言葉に触れると、エヴァンス氏の『エリック・エヴァンスのドメイン駆動設計』や増田亨氏の『現場で役立つシステム設計の原則』といった書籍が頭に浮かびます。私自身、過去のプロジェクトでドメインモデリングを実践してきたものの、その判断軸を体系的に言語化したことはありませんでした。本記事はその試みです。

本記事で扱うのは、業務概念をプログラミング言語の型として表現する設計判断です。
具体的にはEntity、Value Object、ドメインサービスをどう使い分け、業務上の不変条件をどこに置くか、という話を扱います。素材は私が過去に関わった会員制サービスのリプレイス案件です。大規模なDBの部分再設計を通じて業務概念をモデリングし直した経験を、判断軸として整理し直します。

想定読者はテックリード・アーキテクト層です。ドメインモデリングの基本概念は把握している前提で、筆者の現場での具体的な判断を提示します。
本記事はドメインモデル3部作の1本目で、続編では「ビジネスルールを発見する」「ドメインモデルとは何か」を扱う予定です。


第1部: 概念の整理

第1章: 業務概念を型として表現するとは何か

「業務概念を型として表現する」という言葉は、何を指しているか。本記事の出発点としてこの言葉の意味を整理します。

業務概念とは、サービスが扱うビジネスドメインの中で意味を持つ概念のことです。
たとえばECサイトなら「注文」「商品」「在庫」「配送」が業務概念にあたります。
本記事で素材とする会員制サービスでは、「会員」「ポイント残高」「特典」「利用履歴」「会員ランク」などが業務概念です。これらは業務上の関心事であり、システムが扱うべき情報の塊でもあります。

業務概念を型として表現するとは、これらの概念をプログラミング言語のクラスや型として明示的に定義することです。
たとえば「会員ランク」を文字列やintで扱うのではなく、MemberRankという独立した型として定義する。「特典有効期限」を単なるDateTimeではなく、BenefitExpirationという型として定義する。こうすることで業務上の意味がコードの中で明示的になります。

ここでドメイン駆動設計(以下DDD)の語彙が登場します。エヴァンス氏の『DDD』ではEntity、Value Object、ドメインサービスといった構成要素が定義されています。これらは業務概念を型として表現する際の基本パターンです。
Entityは識別子を持つ概念、Value Objectは値そのものに意味がある概念、ドメインサービスは特定のEntityやValue Objectに帰属させにくい業務処理を扱う構成要素です。

増田氏の『現場で役立つシステム設計の原則』では、これらの構成要素を日本の現場感覚で扱う方法が示されています。
特に値オブジェクトと区分オブジェクトコレクションオブジェクト業務ロジックを業務概念に集約するパターンは、現場で実装する際の具体的な指針として有用です。区分オブジェクトは業務上の分類や段階を表す値オブジェクト、コレクションオブジェクトは複数の値や要素をひとまとめにした値オブジェクトを指します。
これらはエヴァンス氏の語彙(Value Object)を、現場の実装感覚に合わせて細分化したものと捉えることができます。

本記事ではエヴァンス氏の語彙(Entity、Value Object、ドメインサービス)を主に使いつつ、増田氏の実装的な視点も参照しながら、業務概念を型として表現する判断軸を整理していきます。

第2章: なぜ業務概念を型に落とすのか

業務概念をプリミティブな型(string、int、DateTimeなど)で扱うのと、専用の型として表現するのとでは、何が違うのか。この違いを理解することが、本記事の出発点になります。

プリミティブな型で業務概念を扱う場合、業務ルールはコードのあちこちに散在します。
たとえば「特典の有効期限を過ぎた会員は、その特典を利用できない」というルールがあるとき、DateTimeの比較で実装すると、このルールが必要な場所すべてにif文が登場します。サービスクラス、コントローラー、バッチ処理、ありとあらゆる場所で同じ判定を繰り返すことになります。

このとき起きるのが業務ルールの暗黙化です。コードを読んでも、それが業務上の不変条件なのか、たまたまその場のロジックなのかが区別できなくなります。新しいメンバーがコードを読んだとき、if ($expirationDate < $now)という判定を見て「これは何のための判定なのか」と問わなければ理解できません。業務ルールがコードの言葉で語られていない状態です。

業務概念を型として表現すると、業務ルールを型の中に閉じ込められます。BenefitExpirationという型を定義し、その中に「期限を過ぎたかどうか」「利用が可能か」といった判定メソッドを持たせる。これによって業務ルールの居場所が明確になり、ルールが変わったときの修正箇所も一箇所に集約されます。
これは増田氏が『現場で役立つシステム設計の原則』で繰り返し主張する業務ロジックを業務概念に集約するという設計思想と直接重なります。サービスクラスや手続き的なコードに業務ロジックを書き散らすのではなく、業務概念を表すオブジェクトの中に業務ロジックを集約することで、業務ルールがコードの中で発見しやすくなり、変更にも強くなります。

不変条件の保証も型表現の利点です。
たとえば「会員ランクは定義された区分のいずれか」というルールがあるとき、stringで扱うと、コードのどこかで未定義の文字列が代入される可能性が常にあります。MemberRankという値オブジェクトを作り、コンストラクタで定義済み区分の検証をすれば、それ以降のコードでは値の妥当性を疑う必要がなくなります。ただしこの保証が成り立つには、Value Objectが不変であることが前提です。生成後に値が変更可能だと、コンストラクタでの検証が後から崩れる可能性があるためです。これは過去記事「防御的プログラミングを学ぶ」で扱った契約による設計の話とも繋がります。

可読性の向上も無視できない効果です。function calculatePoints(int $rank, int $count, int $duration)というシグネチャと、function calculatePoints(MemberRank $rank, UsageCount $count, UsageDuration $duration)というシグネチャでは、コードを読んだときの情報量が違います。後者は引数の意味が型の名前で語られています。

一方で型に落とすコストもあります。クラスの数が増える、設計判断が必要になる、初期実装の手間が増える。これらのコストを払うだけの業務上の意味がある概念だけを、型として表現する判断が必要です。
すべての値を型に落とすと過剰設計になります。型に落とす対象を見極めることが、ドメインモデリングの最初の判断です。


第2部: 実例

第3章: 会員制サービスの業務概念をモデリングする

ここからは過去に関わった会員制サービスを素材に、具体的なモデリングの記録を書きます。筆者が現場で実際に行った判断を、可能な範囲で再現します。

この会員制サービスは、運営事業者が提供する継続利用型のサービスで、会員はサービスを利用するごとにポイントを獲得し、会員ランクが変動し、ランクに応じた特典を受けられる、という業務フローでした。運営事業者は会員の利用状況を確認し、サービスの改善や特典設計の判断材料として使います。

業務概念として最初に整理したのは、識別の必要な概念と値そのものに意味がある概念の区別です。

識別が必要な概念、つまりEntityとして扱う候補は以下でした。

  • 会員(同姓同名でも別人として扱う必要がある)
  • 利用履歴(同じ会員が複数回サービスを利用する)
  • 特典マスタ(特典の種類ごとに異なる)
  • ポイント残高(会員ごとに固有の残高が紐づく)

これらはどれも識別子を持ち、ライフサイクルがあり、状態が変化する概念です。

値そのものに意味がある概念、つまりValue Objectとして扱う候補は以下でした。

  • 会員ランク(ブロンズ・シルバー・ゴールド・プラチナの区分だが、業務上の意味を持つ)
  • ポイント(数値だが、業務ルールが付随する値)
  • 特典有効期限(日時だが、業務ルールが付随する)
  • 利用内容(利用履歴に含まれる業務情報)
  • 特典付与条件難易度(高・中・低の3段階)

これらは識別子を持たず、値の組み合わせで意味が決まる概念です。

ここで重要な判断がありました。
「会員ランク」を最初は単なる文字列で扱おうとしていましたが、業務ヒアリングを通じて、会員ランクには「区分」としての意味があると分かりました。
ブロンズとシルバーの差は、単純な文字列の差ではなく、業務上の意味の差です。ブロンズは「初級会員」、ゴールドは「中級会員」、プラチナは「上級会員」、各ランクで利用可能な特典の範囲やポイント還元率が異なる、という業務的な解釈が付随します。
この発見により、会員ランクは単なる文字列ではなく独立したValue Objectとして表現する判断に至りました。これは増田氏が『現場で役立つシステム設計の原則』で扱う区分オブジェクトの典型例にあたります。区分オブジェクトとは、業務上の分類や段階を表す値オブジェクトのことで、単なる数値や文字列では表現しきれない業務的な意味を型として持たせる構成要素です。

ドメインサービスとして扱う候補も整理しました。

  • ランク判定プロセス(会員のポイント残高や利用履歴から会員ランクを決定する)
  • 特典通知プロセス(ランク変動や特典付与を会員に通知する)
  • ランクダウン判定プロセス(一定期間の利用がない会員のランクを下げる判定をする)

これらはどれも、特定のEntityやValue Objectに帰属させにくい横断的な処理でした。

ランク判定プロセスを例に取ります。
このプロセスは、会員のポイント残高、利用履歴、ランク判定ロジックの3つを参照し、会員ランクを生成・更新します。これを「会員」Entityのメソッドにすると、会員が他の概念を多く参照する形になり、責任が肥大化します。「利用履歴」Entityのメソッドにしても同様の問題が起きます。
そのためランク判定プロセスは独立したドメインサービスとして実装する判断をしました。ただし、ドメインサービスは業務ロジックを業務概念に集約しきれない場合の選択肢として位置付けるのが望ましく、現時点の筆者であれば、業務概念への集約をより深く検討した上で採用するアプローチを取ると思います。

第4章: 既存DB設計から型表現への移行

この案件は新規開発ではなくリプレイス案件でした。既存システムが長年稼働しており、大規模なDBが存在していました。このDBの一部を再設計しながら、業務概念を型として表現し直すというのが、筆者の現場での実際の作業でした。
ここで意識していたのは、DBテーブルとドメインモデルは関心が別である、という視座です。DBテーブルは永続化に特化した構造であり、ドメインモデルは業務課題の解決と整合性に特化した構造です。両者の関心を分離することで、それぞれの構造が本来の役割に集中できる、というのが筆者の判断軸でした。

既存DB設計から型表現への移行は、3つのフェーズで進めました。

第1フェーズ: 既存テーブルから業務概念を抽出する

既存のDBは長年の運用の中で、業務上の意味とテーブル構造のズレが蓄積していました。たとえば「会員情報」テーブルには、会員の属性だけでなく、最新の利用履歴の情報や、最新のポイント残高のサマリーまで含まれていました。これは正規化の観点でも問題でしたが、ドメインモデリングの観点ではより深刻な問題でした。一つのテーブルに複数の業務概念が混在しており、業務ルールがどこに帰属するかが曖昧だったのです。

このフェーズでやったことは、既存テーブルを業務概念ごとに分解する作業です。会員の属性情報、会員のアカウント情報、利用履歴、ポイント残高、これらを別の業務概念として識別し、テーブル設計とコードの両方を整理し直しました。ER図を作りながら、業務ヒアリングと並行で進めました。

第2フェーズ: カラム整理とリレーションの再設計

業務概念ごとに分解されたテーブル群について、カラム定義を業務上の意味に合わせて整理し直しました。具体的には、業務概念にとって必須のカラム、オプショナルなカラム、別の業務概念に分離すべきカラム、を見極めていきました。

ここで判断が分かれたのが、業務上の不変条件をDBレベルでどこまで担保するか、という論点でした。
たとえば「ポイントの付与日は会員登録日より後」というルールを、DBのCHECK制約で担保するか、アプリケーションレベルで担保するか。筆者はアプリケーションレベルでValue Objectのコンストラクタチェックとして実装する判断をしました。
理由は、業務ルールがコードの中で明示的に語られる方がルールの所在が分かりやすいと判断したためです。DB制約に頼ると、ルールがコードから見えなくなる懸念がありました。

第3フェーズ: APIエンドポイント設計(Swagger)との接続

業務概念を型として表現したので、APIのリクエスト・レスポンスもその型に合わせて設計しました。
リクエストボディに含まれる「会員ランク」は、APIスキーマでも独立した型として定義し、定義済み区分の検証をスキーマレベルで担保しました。これによってAPIの利用者にも業務概念の語彙が伝わる形になりました。

このプロセスで最も重要だったのは、筆者一人で進めず、業務ドメインに詳しい運営事業者側の担当者と継続的にすり合わせを行ったことです。業務概念は実装者の頭の中だけで作るものではなく、業務に詳しい人との対話の中で発見していくものだと、改めて感じました。これは過去記事「ドメイン学習の入り口」で書いたドメイン理解の話とも繋がります。


第3部: 判断

第5章: 業務概念を型に落とす判断軸

第3章・第4章で扱った具体例から、業務概念を型に落とす際の判断軸を整理します。筆者が現場で使ってきた判断軸を、ここでは6つに分けて言語化します。

判断軸1: 識別の必要性

業務上、その概念に「同じ性質の別物」を区別する必要があるか、という問いです。
会員は同姓同名でも別人として扱うので識別が必要、つまりEntityです。会員ランク「ゴールド」は他の「ゴールド」と区別する必要がないので識別不要、つまりValue Objectです。この判断軸は、Entity と Value Object を分ける最初の問いです。

判断軸2: ライフサイクルの有無

業務上、その概念が時間とともに状態を変えるか、生成・消滅のプロセスを持つか、という問いです。
利用履歴は「予約」「利用中」「完了」「キャンセル」と状態を変えるのでEntityです。一方、会員ランクは生成された瞬間からその値で固定なので、Value Objectとして扱うのが自然です。

判断軸3: 業務ルールの帰属

その概念に付随する業務ルールが、概念の内側に閉じ込められるか、それとも複数の概念にまたがるか、という問いです。
「特典の有効期限を過ぎたら利用不可」というルールは、特典有効期限と特典マスタの両方にまたがるので、ドメインサービスで扱うのが自然です。一方、「会員ランクは定義された区分のいずれか」というルールは会員ランクの内側に閉じるので、Value Objectのコンストラクタチェックで担保できます。

判断軸4: 不変条件の強度

その概念にとって、ある条件が「絶対に成り立つべきもの」か「ほとんどの場合成り立つもの」か、という問いです。
会員ランク「定義済み区分のいずれか」は絶対の条件なので、Value Objectで型として保証します。一方、「会員の月間利用回数は通常10回以内」のような目安レベルのルールは、型として強制せず、業務ロジックの中で扱う判断もありえます。

判断軸5: 業務上の意味の独立性

その値が、他の値と組み合わさることで意味が変わるか、それとも単独で意味を持つか、という問いです。
「ポイント」は単独で「850ポイント」と言えば業務上の意味を持つので、Value Objectとして独立させる価値があります。一方、「特典の表示順序」のような技術的な値は業務上の意味が薄いので、プリミティブ型のままでも問題ありません。

判断軸6: テストのしやすさ

その概念を型として独立させることでユニットテストが書きやすくなるか、という問いです。
Value Objectとして独立させると、その型の振る舞いを単独でテストできます。プリミティブ型のままだと、テストはその値を使う側に書く必要があり、テストの責務が曖昧になります。

これらの判断軸は独立して使うものではなく、組み合わせて使うものです。一つの業務概念に対して、6つの判断軸を順に問うていく中で、その概念をどう型に落とすかが見えてきます。

判断軸の使い方として、筆者が現場で気をつけていたのは過剰設計を避けることです。すべての値を型として独立させると、クラスが爆発的に増え、コードベースが重くなります。型に落とす価値が高い概念から始めて、必要に応じて広げていく、という段階的なアプローチが現実的でした。


第4部: 反省

第6章: 振り返ると、できなかったこと・課題が残ったこと

この案件の経験を振り返ると、業務概念を型として表現する取り組みは部分的な成功でした。完全に達成できたとは言えない箇所がいくつもあります。本章ではその反省を率直に書きます。

反省点の一つ目は、ドメインサービスの設計が不十分だったことです。
第3章で扱ったランク判定プロセス、特典通知プロセス、ランクダウン判定プロセスは、独立したドメインサービスとして実装したものの、それぞれが他の概念に深く依存する形になっていました。
特にランク判定プロセスは、複数のValue ObjectとEntityを参照する責務が肥大化し、サービスクラスがかなりのサイズに膨らみました。本来であれば、業務ロジックを業務概念に集約することをより深く検討し、ドメインサービスへの依存を最小化する設計を目指すべきでした。

二つ目の反省は、Aggregate(集約)の境界設計が曖昧だったことです。
エヴァンス氏の『DDD』ではAggregateという構成要素が重要視されていますが、当時の筆者のAggregate理解が浅く、明示的な境界設計をしませんでした。
結果として、Entityの一部がAggregate Rootの責務を持ちながらも、それが暗黙的な状態に留まりました。複数のEntityを更新する処理で整合性をどう担保するかの判断が現場で都度発生し、設計の一貫性を欠きました。

三つ目は、業務ルールの一部がコード以外の場所に散在してしまったことです。
業務概念を型として表現するという方針は持ちつつも、現実には全てのルールを型の中に閉じ込められませんでした。
たとえば「ランクダウンは前回利用から一定期間経過後に発生する」というルールは、コード上はドメインサービス内で実装されていましたが、その背景にあるビジネス上の判断(なぜその期間なのか)はコメントだけでは表現しきれず、設計書を参照する必要がある状態でした。

これらの反省は、現時点の筆者であれば違うアプローチを取れる箇所が多くあります。Aggregate設計を最初から組み込む、ドメインサービスの責務分割をより細かくする、業務ルールの背景もコードの中で表現する仕組みを作る、などです。
ただし、当時の筆者の知識と経験では、上記の判断が現実的な落とし所だったとも感じています。設計判断は、その時点での理解の限界によって規定されるものでもあります。

筆者のドメインモデリングの実践は、この案件で完成したわけではなく、今も続いています。本記事を書くこと自体が、筆者の中で過去の判断を振り返り、次の現場で活かす機会になっています。


第7章: シリーズ主軸との接続と、次回予告

本記事では業務概念を型として表現する判断軸を、筆者の現場経験を素材に体系化しました。

シリーズ全体を通じて書いてきた「設計判断は人間に残る」という主張は、本記事の領域でも当てはまります。
業務概念を型に落とす判断、Aggregate の境界を引く判断、業務ルールの帰属を決める判断、これらはすべて筆者とチームが業務ドメインの理解に基づいて行うものです。この判断を支える語彙が、本記事で扱ったドメインモデリングの構成要素でした。

ドメインモデル3部作の次回は「ビジネスルールを発見する」を扱う予定です。本記事では業務概念を型として表現する話を扱いましたが、その前段にある「業務ドメインの中からビジネスルールを発見する作業」を、続編で改めて掘り下げます。3部作の最後は「ドメインモデルとは何か」というテーマで、エヴァンス氏らの古典を現代の実装文脈で読み直す試みになります。

業務概念を型として表現する作業は、筆者にとって今も学び続けている領域です。本記事で書いた判断軸も、今後の現場経験の中で更新されていくはずです。

Discussion