ドメイン記述ミニ言語
Domain Modeling Made Functional (邦訳『関数型ドメインモデリング』)で紹介されているドメインのドキュメンテーションを元にミニ言語を定義する。 基本ルール
この言語では、キーワードと記号を組み合わせて業務の概念を表現する。
data: データ構造を定義する
behavior: 振る舞いを定義する
=: 定義の左辺と右辺を結ぶ
AND: 複数の要素がすべて必要であることを示す
OR: 複数の「場合」のうち、どれか1つが選択されることを示す
->: 振る舞いの入力から出力への変換を示す
//: コメント(補足説明)を示す
単純な値
単純な値は、基本的な型(文字列、整数、数値など)に対して「制約」を付けて定義する。制約はコメントとして自然言語で書く。
code:単純な値
// 2〜100文字
data 名前 = 文字列
// 0〜150
data 年齢 = 整数
// 0以上
data 価格 = 整数
単位を持つ値
物理的な量を扱う場合は、単位を型として分けておくと混同を防げる。
code:単位を持つ値
// 1以上
data 個数 = 整数
// 0.001以上
data キログラム = 数値
// 0以上、円単位
data 金額 = 整数
複合データ
複合データは、複数の要素を組み合わせて、より複雑な業務概念を表現するデータ構造である。単純な値を組み合わせることで、実際の業務で扱う情報をより正確にモデル化できる。
複合データを組み立てる基本は、AND と OR の2つの演算子である。これらを使い分けることで、業務上の「すべて必要」「どれか1つ」という概念を明確に表現できる。
AND: 「全部そろっている」ことを表す
AND は、構成要素がすべて必要であることを示す。例として、「注文」は注文番号と住所がそろって初めて注文として扱える、とする。
code:注文
data 注文 = 注文番号 AND 住所
入れ子にして分解する
複合データは、構成要素を別の data として分解できる。この分解は任意の作業ではなく、ドメインの概念を確定させるために必要なプロセスである。
「住所」や「顧客」といった概念だけでは、業務上どのような要素から構成されるのかが明確にならない。単純な値まで分解することで、初めて具体的な意味と制約が定まり、ドメインモデルとして完全な定義になる。
code:分解
data 注文 = 注文番号
AND 顧客
AND 配送先住所
AND 請求先住所
AND List<注文明細>
AND 金額総計
data 顧客 = 顧客ID
AND 名前
AND 連絡先
data 配送先住所 = 郵便番号
AND 都道府県
AND 市区町村
AND 番地
data 注文明細 = 製品コード
AND 注文数量
AND 価格
// "ORD-"で始まり、その後に10桁の数字
data 注文番号 = 文字列
// "C"で始まる8桁の英数字
data 顧客ID = 文字列
// 2〜100文字
data 名前 = 文字列
data 連絡先 = メールアドレス OR 電話番号
// RFC 5322に準拠
data メールアドレス = 文字列
// 10〜11桁の数字、ハイフンなし
data 電話番号 = 文字列
// 0〜9999999
data 金額総計 = 整数
// 7桁の数字、ハイフンなし
data 郵便番号 = 文字列
// 都道府県名(47都道府県のいずれか)
data 都道府県 = 文字列
// 1〜100文字
data 市区町村 = 文字列
// 1〜200文字
data 番地 = 文字列
// 大文字アルファベット2文字 + 4桁の数字
data 製品コード = 文字列
data 注文数量 = 個数 OR キログラム
// 1以上
data 個数 = 整数
// 0.001以上
data キログラム = 数値
// 0以上
data 価格 = 整数
ここまで「複合データを単純な値まで分解する」という説明をしてきたが、逆の見方もできる。つまり、単純な値を組み合わせて(合成して)、より大きな業務概念を構築していく、という視点である。
分解と合成は同じモデルを異なる方向から見ているだけであり、どちらの視点も重要である。
分解の視点: 大きな概念を理解可能な小さな要素に分けていく(トップダウン)
合成の視点: 小さな要素を組み合わせて大きな概念を作り上げていく(ボトムアップ)
実際のモデリング作業では、両方の視点を行き来しながら、業務の構造を明らかにしていく。
コレクション
同種の要素が複数ある場合は List<...> で表す。
code:注文明細をコレクションとしてもつ注文
data 注文 = 注文番号 AND 顧客情報 AND List<注文明細>
List は「配列」などの実装構造を指定するものではなく、「複数ある」という業務上の意味を表すための記号だと考える。件数の制約(最小件数・最大件数など)がある場合はコメントで補足する。
code:書籍の著者は1人以上でなくてはならない
data 書籍 = ISBN
AND タイトル
AND List<著者> // 著者は1人以上
OR: 「どれか1つの形」を表す
OR は、複数の「場合(形)」から、どれか1つを選ぶ(他は選べない)ことを示す。例として、連絡手段が「メールアドレス」「電話番号」「住所」のいずれか、という場合である。
code:連絡先
data 連絡手段 = メールアドレス OR 電話番号 OR 住所
同じ名前でも、形が違うと中身が変わるときに OR が効く。注文数量が「個数」か「キログラム」かで意味が変わる例である。
code:数量は個数かキログラム
data 注文数量 = 個数 OR キログラム
// 1以上
data 個数 = 整数
// 0.001以上
data キログラム = 数値
支払い方法のように、種類ごとに必要な情報が変わる場合も同様である。
code:支払い方法
data 支払い方法 = クレジットカード OR 銀行振込 OR 代金引換 OR コンビニ決済
data クレジットカード = カード番号 AND 有効期限 AND セキュリティコード
data 銀行振込 = 振込先口座 AND 振込期限
data 代金引換 = 手数料
data コンビニ決済 = 支払い番号 AND 支払い期限
「少なくとも1つ」は OR ではなく List で表す
実務では「メールも電話も持ってよいが、少なくとも1つは必須」のような要件がよく出る。このとき表したいのは「場合が1つに決まる」ではなく「複数持てる」なので、OR ではなくコレクション+件数制約で表す。
code:連絡手段を少なくとも1つ持つ顧客
// 顧客は、連絡手段を少なくとも1つ持つ
data 顧客 = 氏名
AND List<連絡手段> // 連絡手段は1件以上
または、メインとサブに分けて扱う設計もありえる。
code:連絡先はサブを持つこともある
// 顧客は、連絡手段を少なくとも1つ持つ
data 顧客 = 氏名
AND メイン連絡手段
AND List<サブ連絡手段>
data メイン連絡手段 = 連絡手段
data サブ連絡手段 = 連絡手段
オプショナル
必須ではなく「ある場合とない場合がある」要素には ? を付ける。
code:任意の項目
data 顧客 = 氏名
AND メールアドレス?
AND 電話番号?
data 配送 = 注文
AND 配送希望時間?
AND 住所
ただし、安易に ? を付けると、業務上あってはいけない状態まで許してしまうことがある。例えば「連絡先がまったくない顧客は許容しない」なら、次のようにモデル化した方が意図が明確である。
code:状態異常が無いように設計する
data 顧客 = 氏名 AND 連絡先
data 連絡先 = メールアドレス OR 電話番号
振る舞い
「振る舞い(behavior)」は、業務上の処理や操作を表現するものである。振る舞いは、入力となるデータを受け取り、処理を行い、出力となるデータを返す。
code:振る舞い
behavior 振る舞い名 = 入力データ -> 出力データ
code:振る舞いの例
behavior 受注する = 注文者 AND 商品 -> 注文
behavior ユーザーを登録する = 名前 AND メールアドレス AND 電話番号 AND 住所 -> ユーザー
behavior 価格を計算する = 商品 AND 数量 -> 金額
behavior 在庫を確認する = 製品コード -> 在庫数量
成功と失敗を明示する
現実の処理は、常に成功するとは限らない。その場合、OR を使って結果のパターンを明示する。
code:失敗の明示
behavior 注文を検証する = 未検証注文 -> 検証済み注文 OR 検証エラー
behavior メールアドレスを検証する = 未検証メールアドレス -> 検証済みメールアドレス OR 検証エラー
behavior 在庫を引き当てる = 製品コード AND 数量 -> 引当済み OR 在庫不足エラー
ドメイン記述の例
このミールス宅配サービスを例に、ドメインのdataとbehaviorを表現したものを例に示す。
code:ミールス宅配
// ユーザー管理(メール認証なしの簡易登録)
data ユーザ = 名前
AND メールアドレス
AND 電話番号
AND 住所
data 住所 = 郵便番号
AND 都道府県
AND 市区町村
AND 番地
// 定期便の基本構造(3種類のプラン選択と配送頻度)
data 定期便 = ユーザ
AND プラン
AND 配送頻度
// 定期便の状態管理(契約の一時停止・再開を可能にする)
data 定期便 = アクティブ定期便 OR 一時停止定期便
// アクティブな定期便から次回の注文予定を自動生成
behavior 次回注文予定を算出する = アクティブ定期便 -> 次回注文予定
// 現在の次回注文予定を取り消して、次の注文予定を新たに作る
// プラン変更は即時反映される
behavior プランを変更する = 定期便 AND 変更プラン AND 次回注文予定 -> 定期便 AND 次回注文予定
// 次回配送予定を削除する
// 最低契約期間なしで、いつでも一時停止可能
behavior 定期便を一時停止する = アクティブ定期便 AND 次回注文予定 -> 一時停止定期便
behavior 定期便を再開する = 一時停止定期便 -> アクティブ定期便 AND 次回注文予定
// 食材セットの手動登録を前提とした構造
data ミールセット = 名前
AND 説明
AND List<ミールセット明細>
data ミールセット明細 = ミール
AND 数量
// 冷凍品追加料金の計算に必要な保管方法
data 保管方法 = 冷凍 OR 常温
data ミール = 名前
AND 保管方法
// 注文予定は配送日3日前まで変更可能な期間に存在
data 注文予定 = アクティブ定期便
AND 注文確定予定日
// 注文確定予定日になると自動的に注文が確定される
behavior 注文確定する = 注文予定 -> 注文
data 注文 = ユーザ
AND 住所
AND List<注文明細>
data 注文明細 = ミールセット
AND 数量
// 代金引換決済のため、注文確定時に請求が生成される
behavior 請求する = 注文 -> 請求
// 請求書の内訳(地域別送料、冷凍品追加料金等を含む)
data 請求 = 注文
AND 請求金額
AND 送料
AND 冷凍手数料
AND 代引き手数料
AND 時間指定手数料
AND 税率
// 配送時間帯指定(午前/午後)
data 配送希望時間 = 午前 AND 午後
// 配送リストの手動生成と配送状況の管理
data 配送 = 注文
AND 配送希望時間?
AND 住所
// 納品書・請求書の印刷機能のための出荷処理
behavior 出荷する = 注文 -> 配送
例えばAI Agentに次のようなシステムプロンプトを元に、ドメインモデルをエンコードさせることを目論む。 code:システムプロンプト.md
## ドメイン層のグランドルール
ここでは、ドメイン層で扱うデータは業務上常にValidなものでなくてはなりません。
ドメイン層で定義される振る舞いは、原則的に全域性を満たす必要があります。
全域性とは取りうる全ての入力に対して、例外を送出することなく、出力を返すことを意味します。 - (A)
さらには振る舞いへの入力は、極力使われないものが含まれないようにする必要があります。 - (B)
(A)と(B)を満たすためには、型を分解する必要があるかもしれません。
## ドメインモデルの実装
ドメインモデルのDSLでは、データ構造が入れ子で表現されることがありますが、必ずしもその通りの型を作らなくても良いです。
特に振る舞いで使われないデータは、実装段階では型定義する必要はありません。
## 命名規約
ドメイン層の型、および振る舞いの名前は、短く簡潔で、PrefixやSuffixは不要です。
## データベースアクセス
データベースへのアクセス関数は、ドメイン層のデータを入出力とします。データベースのアクセス関数の中で適切にマッピングします。
モデルを使って対話するために
フラグではなくORで表現する
プログラミングでは、真偽値(boolean)やフラグで状態を表すことがよくある。
code:承認済みフラグ
data 注文 =
注文番号
AND 顧客名
AND 商品リスト
AND 承認済みフラグ // true: 承認済み, false: 未承認
しかし、この書き方には次のような問題が出やすくなる。
状態の意味が「true/false」に埋もれてしまう
状態が増えると表現しづらい
業務上ありえない組み合わせを、構造としては防げないことがある
そこで、フラグの代わりに OR を使って状態(場合)を明示する。
code:jsx
data 注文 = 未承認注文 OR 承認済み注文
data 未承認注文 =
注文番号
AND 顧客名
AND 商品リスト
data 承認済み注文 =
注文番号
AND 顧客名
AND 商品リスト
AND 承認者
AND 承認日時
このように書くと、
状態が業務用語のまま表現される
状態ごとに必要なデータが自然に分かれる
新しい状態を足しやすい
といった利点がある。
code:jsx
data 注文 = 未承認注文 OR 承認済み注文 OR 差し戻された注文 OR キャンセルされた注文
ステータスコードではなく「型」と「遷移」を明示する
実務では、状態を数値や文字列のステータスコードで表すこともよくありる。
code:jsx
data 注文 =
注文番号
AND 顧客名
AND 商品リスト
AND ステータス // 0: 下書き, 1: 未承認, 2: 承認済み, 3: 出荷済み, 4: 完了
状態を「場合の列挙」として書く
code:jsx
data 注文 =
下書き注文
OR 未承認注文
OR 承認済み注文
OR 出荷済み注文
OR 完了した注文
遷移(できる操作)を振る舞いとして書く
状態が変わる操作を、入力と出力で表現する。
code:jsx
behavior 注文を承認する = 未承認注文 AND 承認者 -> 承認済み注文 OR 承認失敗
behavior 注文を出荷する = 承認済み注文 AND 配送先 -> 出荷済み注文 OR 出荷失敗
behavior 注文を完了する = 出荷済み注文 -> 完了した注文
この形にすると、
どの状態から、どの状態へ移れるのか
その操作に何が必要なのか
が仕様として読み取りやすくなる。
分岐の条件を「名前」で表現する
実際の業務には、状態とは別に「条件で仕分けする」ルールも多く含まれる。
ここでは、似ている2つを分けて考えます。
状態(ライフサイクル): 時間や操作で変わるもの(例: 未承認 → 承認済み → 出荷済み)
区分(分類): その時点のデータを条件で分けた結果(例: 金額が「高額」か「通常」か)
条件がコードに埋もれる問題
典型的なコードでは、条件はif文の中に直接書かれる。
code:jsx
if (order.amount >= 100000) {
// 承認が必要
} else {
// 承認不要
}
このままだと、仕様で話している「高額注文」という言葉がコード上から消え、数字だけが残りやすくなる。さらに、同じ条件が複数箇所に散らばって重複する。
ルール(区分)に名前を付ける
そこで、条件の判定結果に名前を付ける。
code:jsx
// 高額: 金額が10万円以上
behavior 高額か判定する = 金額 -> 通常 OR 高額
こうしておくと、以後は「通常」や「高額」という言葉で仕様を話せるようになる。
ほぼ同じ data が複数あるときの表現指針
実務では、既存の data に項目が少しだけ追加されたバージョンが出てきたり、将来の拡張を見越して似た data を複数用意したくなることがよくある。
しかし、ここで表現を誤ると、仕様が次のように劣化しがちである。
注文 / 注文v2 / 注文3 のような、名前だけ変えたコピペが増える
Optional(?)で差分を吸収してしまい、業務上ありえない状態まで許してしまう
「今回は使わない項目」など、コメントに仕様を預けてしまう
大きな粒度でコピペして増やすのではなく、共通部分を切り出して合成するという姿勢が重要である。
コピペは、差分の理由を「文章の外(記憶と命名)」に逃がす。
合成は、差分の理由を「モデルの中(構造と名前)」に残す。
ドメイン記述ミニ言語では、再利用は継承ではなく 合成(AND) として扱う。同じものが繰り返し出てくるなら、まず 共通data を作り、差分はそこに 追加で AND する。目標は「似た data を減らす」ではなく、似ている理由と違う理由を、構造として読めるようにすることである。「ほぼ同じ data」が現れたときの判断を、3つの観点で整理します。
最初に確認する:その差分は何の差か
「ちょっとだけ違う」と感じたら、次を順に確認する。
1. 必須項目が違うか
2. 不変条件(許されない状態)が違うか
3. 業務上の役割・段階が違うか
後ろの問いほど抽象度が高いので、前を飛ばすと判断を誤りやすくなる。
ここでいう3つの問いは、同じ「差分」でも、何を確定したい差分なのかが異なるために分けている。
必須項目は「存在しないと業務として成立しない情報」である。ここが違うなら、その差分は仕様として強く、? で隠すと仕様が崩れやすくなる。
不変条件は「同時に成り立ってはいけない組み合わせ」である。見た目の項目は同じでも、許されない状態が変わるなら、モデルを分ける(または制約を明記する)必要が出る。
業務上の役割・段階は「同じ情報でも、扱える操作や責務が違う」差分である。ここが違うなら、データだけではなく振る舞いまで含めて状態として捉える。
差分が「必須項目」に現れる場合:共通を抽出して合成する
必須項目が違う場合、その違いは仕様として強い意味を持つ。
ただし、毎回「全体を OR で分ける」ほどでもないことも多いので、その場合は共通部分を切り出し、差分を AND で合成する。
例として、注文に「承認」が付く前後を考える。
code:差分をANDで合成する
// 共通部分
// 注文番号と顧客IDと明細がそろっていること
data 注文共通 = 注文番号
AND 顧客ID
AND List<注文明細>
// 未承認注文には見積有効期限が必須
data 未承認注文 = 注文共通
AND 見積有効期限
// 承認済み注文には承認者・承認日時が必須
data 承認済み注文 = 注文共通
AND 承認者
AND 承認日時
この形にしておくと、
「ほぼ同じ」ことが構造として明示される
差分がどこか一目で分かる
差分が増えても影響範囲が局所化される
といった利点がある。
差分が「局所的な選択肢」の場合:差分を部品として OR に閉じ込める
全体の構造はほとんど同じだけれど、一部だけ違う項目があるとき、その違nいを本体に直接混ぜてしまうと、仕様が読みにくくなる。その場合は、違う部分だけを OR で別に定義する。
例として、配送方法だけが違う注文を考える。
code:配送方法だけが異なる
// 差分だけを OR に閉じ込める
// 配送方法はどれか1つ(他は選べない)
data 配送方法 = 通常配送 OR 冷蔵配送 OR 冷凍配送
data 注文 = 注文番号
AND 顧客ID
AND List<注文明細>
AND 配送方法
この形なら、配送方法の追加・変更が局所化される。
一方で、次のように任意性で吸収すると「差分が隠れる」ため避ける。
code:jsx
// 避けたい例:差分が構造に出ない
data 注文 = 注文番号
AND 顧客ID
AND List<注文明細>
AND 冷蔵フラグ?
AND 冷凍フラグ?
差分が「段階・責務」の違いの場合:状態として分離する
形はほぼ同じでも、
できる操作が違う
次に進める処理が違う
という違いがある場合、その差分は「データの差」というより状態(ライフサイクル)の差である。
例として、下書きと確定後の注文を考えます。
code:注文の状態によって不変条件が異なる
data 注文 = 下書き注文 OR 確定済み注文
data 下書き注文 = 注文番号 AND List<注文明細>
data 確定済み注文 = 注文番号 AND List<注文明細> AND 確定日時
behavior 注文を確定する = 下書き注文 -> 確定済み注文
behavior 明細を変更する = 下書き注文 AND 変更内容 -> 下書き注文
「項目が少し増える」だけに見えても、実務上は責務と操作が変わることが多いので、状態として分けた方が読みやすくなります。
差分は「どこ」ではなく「なぜ」で見る
「ちょっとだけ違う data」が出てきたときに見るべきなのは、どこが違うかではなくなぜ違うのかである。
必須項目が違うなら → 共通抽出+合成(AND)
局所選択肢なら → 差分の部品化(OR)
段階・責務なら → 状態として分離(OR+振る舞い)
参考: Domain Modeling Made Functionalオリジナル
ワークフローの記述
code:workflow
Bounded Context: 受注
ワークフロー: "注文する"
トリガー: "注文書を受け取った"イベント (チェックボックス「見積」が未チェックの場合)
主要インプット:
注文書
他のインプット:
製品カタログ
出力イベント
"受注した"イベント
副作用:
受注内容に沿って、顧客に受注通知が送信される。
ワークフロー名を動詞で書く。
「トリガー」にこのワークフローを起動するイベントを動詞(過去形)で書く。
「主要インプット」にワークフローが参照するインプットのうちもっとも主要なものを書く。
それ以外のインプットを「他のインプット」に書く。
ワークフローの実行の結果、出力されるイベントを「出力イベント」に動詞(過去形)で書く。
このワークフローの実行の結果、コンテキストの外部へ影響を及ぼすものを「副作用」に書く。
ワークフローに関連するデータ構造の記述
code:data-structures
Bounded Context: 受注
data 注文 =
顧客情報
AND 配送先住所
AND 請求先住所
AND 注文明細のリスト
AND 金額総計
data 注文明細 =
製品コード
AND 注文数量
AND 価格
data 顧客情報 = ??? // 現時点で詳細不明
data 請求先住所 = ??? // 現時点で詳細不明
data 注文数量 = 個数 OR キログラム
data 個数 = 整数 (1 ~ ?)
data キログラム = 数値 (? ~ ?)
dataディレクティブでワークフロー記述中に登場する名詞を定義する。
dataを構成する要素を分解して記述する。
dataの構成要素をさらにdataとして詳細を記述する。(dataが標準型と制約から構成されるところまでブレイクダウンする)
不明なものは後で詳細化できるように「???」としておく。
dataに制約が含まれるものはそれを記述する。
上記例で「注文」という単一のデータ構造だと、価格情報をもっているのでワークフロー"注文する"の主要インプットである注文書は表現できない。(この業務では受注処理時に担当者が製品カタログをみて最新の価格を記載する)
また、注文書を受け取った段階では、顧客情報や住所も未チェックなので、厳密にはワークフロー内でチェック済みのそれとはデータ構造は区別したい。
code: data-structures
data 未検証注文 =
未検証顧客情報
AND 未検証配送先住所
AND 未検証請求先住所
AND 未検証注文明細のリスト
data 未検証注文明細 =
未検証製品コード
AND 未検証注文数量
data 検証済み注文 =
検証済み顧客情報
AND 検証済み配送先住所
AND 検証済み請求先住所
AND 検証済み注文明細のリスト
data 検証済み注文明細のリスト =
検証済み製品コード
AND 検証済み注文数量
data 金額記載済み注文 =
検証済み顧客情報
AND 検証済み配送先住所
AND 検証済み請求先住所
AND 金額記載済み注文明細のリスト
AND 金額総計
data 金額記載済み注文 =
検証済み注文明細のリスト
AND 価格
ワークフロー擬似コード
ワークフロー内部がいくつかの入力→サブステップ→出力の連鎖で構成される場合、サブステップを擬似コードを使って書き出す。
code: workflow-pseudo-code
workflow "注文する" =
input: 注文書
output:
"受注した"イベント
OR 不正な注文
// step1
注文を検証する。
もし注文が検証に失敗したら、注文書を不正な注文の山に加えて、ワークフローを停止する。
// step2
注文に価格を記載する。
// step3
顧客に受注通知を送る。
// step4
"受注した"イベントを返す。
code:substep1-pseudo-code
substep "注文を検証する" =
input: 未検証注文
output: 検証済み注文 OR 検証エラー
dependencies: 製品コード実在チェック, 住所実在チェック
顧客名を検証する。
出荷先住所と請求先住所が実在するかをチェックする。
注文明細各行に対して:
製品コードのシンタックスをチェックする。
製品コードが製品カタログに存在することをチェックする。
すべてがOKであれば:
検証済み注文を返す。
そうでなければ:
検証エラーを返す。