ドメイン駆動設計(DDD)と聞くと、難しい用語が並ぶ設計手法という印象があると思います。ただ、出発点は素朴です。業務で使っている言葉を、そのままコードの名前にする。これだけです。
とはいえ抽象的な話だけでは伝わらないので、今回も動かしました。「顧客」という同じ言葉を1つのモデルにまとめたコードを書いて、販売部門と配送部門の両方から登録してみます。結果を先に言うと、どちらの部門も登録できませんでした。
業務の中身をよく理解したうえで、その理解をコードの形にする手法です。用語は多いのですが、土台にあるのはこの一点だけです。
ドメイン駆動設計は、業務の手順や規則をよく理解したモデルを中心に据えて開発する手法です。ドメインとはそのソフトウェアが扱う業務の領域のことで、請求なら請求の、予約なら予約の世界を指します。
2003年のEric Evansの本から広まった考え方で、その後も実務家によって育てられてきました。特に込み入った業務ほど効くとされています。
DDDの中心にあるのは、業務で使われている言葉をソフトウェアの中に埋め込むことです。Fowlerはこれをユビキタス言語と呼び、開発者と業務の担当者のあいだで共通の、厳密な言葉を作る営みだと説明しています。
なぜ厳密でなければならないかというと、ソフトウェアは曖昧さをうまく扱えないからです。会話なら「だいたい同じ意味」で流せますが、コードにするとその曖昧さがそのままバグになります。
モデルを作ること自体は昔からありました。DDDが強調したのは、それを紙の上ではなくソフトウェアの中で作り、動かしながら育てることです。最初に完成させて凍結するものではありません。
DDDには集約・値オブジェクト・リポジトリなど多くの用語がありますが、全部を一度に使う必要はありません。土台は「言葉を揃える」ことと、次の節で扱う「意味が変わる場所で区切る」ことの2つで、残りはその上に乗る道具だと編集部は考えています。
Domain-Driven Design is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain.原文Martin Fowler「DomainDrivenDesign」 この内容の有効期限2027-02-17
同じ言葉でも、部門が違えば意味が違います。それを1つのモデルに押し込むとどうなるかを、実際に書いて動かしました。
ドメイン駆動設計で最初につまずくのは、たいていここです。同じ言葉が、部門によって違うものを指しているという事実に気づかないまま設計を進めてしまいます。
Fowlerは、大きな組織では部門ごとに少しずつ違う語彙が使われる、と指摘しています。自身の経験として、電力会社で「メーター」という語が部門ごとに違う意味を持っていた例を挙げています。
手元で試しました。販売部門にとっての顧客は与信の対象で、限度額が要ります。配送部門にとっての顧客は届け先で、住所と時間帯が要ります。同じ「顧客」でも、必要な情報がまるで違います。この2つを1つのモデルにまとめてみます。
下のコードは読み飛ばして大丈夫です。統合したモデルでは、両部門ぶんの項目が全部そろっていないと受け付けない形になります。
// 統合モデル: 販売も配送も Customer 1つで扱う
function validateUnified(c) {
const need = ['name', 'email', 'creditLimit', 'address', 'deliveryWindow'];
return need.filter((k) => c[k] === undefined);
}
// 分割モデル: 販売の Customer と 配送の Recipient を別物にする
function validateSales(c) {
return ['name', 'email', 'creditLimit'].filter((k) => c[k] === undefined);
}
function validateDelivery(r) {
return ['name', 'address', 'deliveryWindow'].filter((k) => r[k] === undefined);
}
--- 統合モデル(Customer 1つで両方を扱う)--- 販売部門の登録: 拒否(不足: address, deliveryWindow) 配送部門の登録: 拒否(不足: email, creditLimit) --- 分割モデル(販売のCustomerと配送のRecipientを分ける)--- 販売部門の登録: 受理 配送部門の登録: 受理
両方とも拒否されました。販売部門は配送情報を持っていないから、配送部門は与信情報を持っていないからです。どちらも自分の業務では正しいデータを入れているのに、登録できません。
統合モデルの必須項目は、両部門の必須項目を足したものになります。するとどちらの部門も、自分には関係のない項目を埋めさせられることになります。
現場でよく起きる対処は、必須をやめて全部を任意にすることです。しかしそうすると、今度はどの項目が本当に必要なのか誰にも分からなくなります。与信の限度額が空でも通ってしまう販売システムができあがります。
統合すると必須項目が足し算になる。分けると各文脈の必須だけで済む。
この「意味が変わる場所で区切る」という考え方が、DDDでは境界づけられたコンテキストと呼ばれます。名前は仰々しいのですが、やっていることは同じ言葉の意味が変わる線を引くだけです。
Different groups of people will use subtly different vocabularies in different parts of a large organization.原文Martin Fowler「BoundedContext」 この内容の有効期限2027-02-17
境界を分ける話は規模が大きいほど効きますが、言葉を揃えることは規模を問いません。個人開発なら後者だけでも十分に効きます。
ドメイン駆動設計の全部を個人開発に持ち込む必要はありません。ただ、言葉を揃える部分だけは規模と関係なく効きます。
Fowlerは、ユビキタス言語を開発者と利用者のあいだで共通の厳密な言葉を作る営みだと説明しています。個人開発なら相手は自分ですが、半年後の自分は他人なので事情は変わりません。
実際にやることは単純です。頭の中で「申込」と呼んでいるものを、コードでは Application ではなく Order と書いていないか。会話とコードで名前がずれていたら、その分だけ読み替えの手間が発生します。
境界を分けるかどうかは、規模ではなく兆候で判断します。次のような状態が見えたら、意味の違う用途が混ざっている可能性があります。
if (type === '販売') が各所に出るなら、分ける合図逆に、これらが出ていないなら分ける必要はありません。分けること自体が目的ではないので、困っていないうちは1つのままで構いません。
設計の考え方としてはクリーンアーキテクチャの記事とも重なります。あちらは依存の向きを扱っていて、こちらは名前と境界を扱っています。
境界を分ける判断は、あとから見ると根拠が分かりにくくなります。なぜ分けたのかを一行でも残しておくと、次に触るときの判断が早くなります。分けた結果として重複するデータについても、どちらが正なのかを書いておくと迷いません。
Ubiquitous Language is the term Eric Evans uses in Domain Driven Design for the practice of building up a common, rigorous language between developers and users.原文Martin Fowler「UbiquitousLanguage」 この内容の有効期限2027-02-17
同じ課題を持つ会社にとって、動いている設定は「作る時間」を買えるということです。ServiceDockは自作のワークフローやテンプレートを出品できるマーケットプレイスです。手数料や出品の流れは出品者向けページにまとまっています。
出品の仕組みを見る