1. はじめに
最初は、クリーンアーキテクチャーやオニオンアーキテクチャーを学ぶ中で「ドメインが最も抽象的で安定的」という考え方が出てきたところから、ドメイン駆動デザイン(DDD)の世界に入りました。
この文脈でDDDを知ったので、てっきり依存をドメイン層に集めるのがDDDの基本で、entityやらvalue object やらを作っておけばそれっぽくなるとばかり考えていました。
そのため、正確なDDDを学んだときあまりに違って驚きました。
驚きが新鮮なうちに、忘備録も兼ねてまとめておこうと思います。
この記事は、以下の書籍を参考にしています。
どんなアーキテクチャーを採用するべきかという点にまでは踏み込まず、この記事ではDDDの概要をさらうことに集中します。
目次
2. 戦略的なDDD
Domain Driven Designという名前の通り、DDDは設計方針を表します。「設計」という概念は「アーキテクチャー」という概念同様に抽象度が高く、プロダクトの開発・運用にいたるまで一貫して影響を与えるものです。
このため、DDDは単純にvalue objectとかentityとかaggregatesとか、あるいはevent sourcingのような具体的な実装パターンにのみ限定されるわけではありません。
むしろより抽象的で、開発の取り組み全体において一貫されるべきものを含んでいるから設計なのであります。
このように一貫した方針決定を支えるものとして、DDDの戦略が出てきます。
DDDの戦略は端的に言うと「業務領域の理解を深め、業務をコード上で表現する」ことを目標とし、そのために業務エキスパートとの密なコミュニケーションを求めます。
2-1. ドメインの決定
ドメイン駆動設計が対象とするのは「業務領域」です。
事業活動を行うために細分化されたのが業務領域であり、それには三つの種類があります。
- 中核
- 補完
- 一般
ドメイン駆動設計が導入の際に主な解決課題とするのは中核的な業務領域の実装ですが、そうするためにもまずは何が中核かを見抜けなければなりません。
業務領域の仕分けに用いる視点は重要性と複雑性です。
重要性はビジネス的な視点で、それが企業の利益を生み出すものであるとか、他の企業では提供できない価値である場合に高いと見做せます。
複雑性はエンジニア的な視点で、それを実装するのが難しかったり時間がかかる場合に高いとみます。
中核的な業務は、複雑かつ重要です。
これが利益の源泉となり、他の企業とは違うものを提供できているから重要となりますし、それは簡単に模倣されてはいけないので複雑であるべきです。
また、中核的な業務領域は変化しやすい、あるいは変化の対象となることも特徴として挙げられます。
Twitter、Youtube、TikTokなどを例にとってみると、彼らにとっての直接の収益源は広告などですが、それを見せるためにもエンゲージメントの向上は必要で、そのための優秀なレコメンド機能が利益の根源と言えるでしょう。
このレコメンド機能が彼らにとっての中核的な業務領域といえるはずです。最近、フェイクニュース対策やフィルターバブル対策などのためにレコメンドのアルゴリズムの透明性が云々という議論がなされていますが、これこそが利益の源となっているSNS運営企業にとって公開は結構無理筋な気がします。
また、このレコメンド機能は常に最適化の対象であり続けるので、常に変化・更新し続けます。この点で行っても、変わりやすいという中核の業務領域の特徴を備えています。
補完的な業務は、重要ではありますが複雑ではありません。
自前でECサイトを構築する例を考えてみます。ECビジネスでは商品の陳列表示や類似商品の推薦が中核的な業務領域だとすると、例えば倉庫での商品管理は直接的には利益を生み出しませんが、中核のビジネスを行う上で必ず必要になります。ここを最適化しても売り上げが増えるわけではありませんが、迅速で正確な管理と配送はユーザー体験を向上する可能性もあります。
そして、補完的な業務は外のライブラリーやシステムに依存するほど複雑ではないので、往々にして自前で実装することになります。
一般的な業務は、重要ではない割に複雑なものです。
自前でECサイトを構築する例をもう一度考えてみます。ECサイトでは必ず決済やアカウント管理が必要になりますが、これを自前で作りこむのはなかなか厄介です。このとき、決済で必要となるシステムにStripe、アカウント管理にオープンソースのJWT認証ライブラリーを入れるなどすれば、時間をかけずに必要な要件を満たすことができます。
言うまでもなく、中核的な業務領域に開発コストを集中的に投じるべきです。
反対に言えば、わざわざ補完的な業務や一般的な業務に過度に複雑な実装方法を採用するべきではありません。
後述することではありますが、ドメインモデルは複雑さに対応するために設計されるものなので、これをわざわざ補完的な業務の実装に使おうとすると「ドメインモデル貧血症」となってしまいます。補完的な業務にはアクティブレコードなどを使うようにするべきです。
全ての業務の実装において、何もドメインモデルなどのDDDで使われる部品を用いる必要があるわけではないことは、注意したいところです。
ただし、これらの業務領域は事業が成長・変化するにつれて分類が変化することがあります。
補完的な業務として挙げた倉庫管理はシステムとしてオープンソースが出てきた場合、それを採用するなら一般的な業務領域になります。
また例えば、「自前のECサイトは振るわなかったもののエンジニアが頑張って倉庫管理システムを最適化し続けた結果、優秀な倉庫管理システムが出来上がり、それを外販してみたら売り上げが伸びた」みたいなときは補完的だった業務が中核的な業務になります。
Amazonは自前のECビジネスも世界レベルになりましたが、サービスを運営するために拡大していたサーバーをクラウドとして外部提供したらこれも収益の柱になりました。
このような変化に対応して、業務領域の篩分けは定期的に行わなければなりません。
このような業務領域の篩分けには、かなりの業務領域の知識が必要になるので、エンジニアチームだけではできない場合が多いと思われます。
この点でも、業務エキスパートとの協働が前提とされています。
2-2. ユビキタス言語
ユビキタス言語は、解決しようとする事業活動にフォーカスしてこれをモデル化したものです。
業務知識が仕様書や設計書となりソースコードになる従来のプロセスでは、業務エキスパートが意図していた業務課題や要求が抜け落ちて行ってしまいます。
従来のプロセスを否定するわけではありませんが、実装したい業務のモデルに対して、わざわざそれぞれの関係者間で独自の専門用語に翻訳することはありません。全体で「同じ言葉」を使うことで、一貫性を保ち業務課題の解決に寄り添ったソースコードが出来上がります。
一般的には「ユビキタス言語」とカタカナ語のままですが、書籍には「同じ言葉」と表現されていました。専門用語であることを端的に表現するためにこの記事では「ユビキタス言語」と言いますが、「同じ言葉」というのはいい表現だと思います。
何と同じ言葉か。
あるドメインを表す単語はコードの中で一貫して「同じ言葉」であるべきだし、業務エキスパート(ドメインエキスパート)と「同じ言葉」であるべきだということです。
このような一貫した言葉遣いにするためには、「エンジニアが業務エキスパートがもっている知識や言葉遣いを理解してそこに合わせる」という、割とシャレにならないくらい(片手間にはできないくらい)重い学習コストとコミュニケーションコストを投じる必要があるのです。
「同じ言葉」
個人的な語感で恐縮なのですが、ubiquitous は「遍在的で誰でも使える」というような感じで意味は分かるのですが、どちらかというとよりaccessible に近い語感で開かれている印象があります。しかし実態としては、エンジニアの側が業務をより深く理解し、業務エキスパート側の言葉にコードの中で用いられる言葉を合わせる形なので、そこまで対等な感じはしません。エンジニアが業務エキスパートと「同じ言葉」を使うと正確に言うことで、このコンセプトが目指すところが具体的に示されます。2-3. 境界付けられたコンテキスト
最近のAIもそうですが、人間も文脈を理解する力があります。多義的な言葉は会話や文章の中での使われ方によって意味が定まります。
例えば、大学での授業・研究活動を管理する自前のシステムをITに強い教授が作ろうと思い立ったとします。
このとき、院生の立ち位置は微妙で、教授の指導の受け手と言う意味で学士生徒と同じような立場にもなりますが、研究を手伝うアシスタント的な役割を担うこともままあります。
日常の会話ではこの二つの異なる側面を使い分けることはたやすいですが、ソースコードの中では非常に分かりにくいものになるでしょう。
よくあるのは「Graduate Student」という基盤モデルから派生させて、「Graduate Student Assistant」みたいなモデルを作るかもしれません。
しかし、院生はTutorとして教授の授業準備や生徒指導の補助も行ってくれる場合があります。ではこのとき、また「Graduate Student Tutor」として派生させるのでしょうか?
DDDにおいては、ユビキタス言語を徹底させる必要があり、そこでは「チューターの」とか「アシスタントの」みたいな修飾語は好ましくありません。業務エキスパートは文脈によって使い分けていますから、それをソースコード上でも再現したいわけです。
「境界付けられたコンテキスト」はそれを可能にします。
「成績評価」という文脈の中で院生が出てきたら明らかに、授業の受け手やゼミでの指導の受け手という意味です。これに対して、「講義準備」などで出てくる院生はチューターですし、「研究活動」で出てくる院生はアシスタントになるでしょう。
コンテキストはその中に複数の業務領域を含むこともあります。
コンテキストはお互いに独立して開発・運用されることになりますが、もしコンテキストのモデル同士を連携させたい場合、共有モデルを作ったり、あるいは一方のモデルに従属する形になるでしょう。後者の場合、利用者か供給者のどちらかが変換装置を作ることでモデル変更の影響を小さくすることができます。
3. 戦術的なDDD
戦略的なDDDを実装するためには、業務エキスパートとの密なコミュニケーションが必要になります。
書籍では、関係者が一堂に会して行われるイベントストーミング(Event Storming)というワークショップの方法論が一章丸々の文量を割いて紹介されていたほどです。
これに対して戦術的なDDDでは具体的な実装に使われるものなので、エンジニアだけで使えるものです。
ただ、繰り返すようですが、複雑性に合わせた適切なモデル構築が必要です。ドメインモデルも銀の弾丸ではなく、中核的な業務領域に対して用いられるべきものであることを忘れないでおいてください。
3-1. ドメインモデル
ドメインモデルは複雑なロジックを扱うための設計手法で、複雑な状態遷移、業務ルール、不変条件など常に保護されるべきルールを扱います。
ドメインモデルはデータと共にドメインロジックも備えます。
部品は以下の通りです。
- Value Object
- Aggregates
- Domain Service
Value Objectは状態変化のない不変なオブジェクトです。一意のidは割り振られず、フィールドの値が一致すれば両者のオブジェクトは同一であるとみなせます。
Value Objectは、Entityの部品として扱われることが多いです。
例えば、StockCodeは以下のように定義しておきます。
@dataclass(frozen=True)
class StockCode:
stock_code: str
stock_exchange: Literal["XNYS", "XNAS", "XJPX", "XLON", "XFRA"]
def valid_stock_code(self) -> bool:
return len(self.stock_code) <= 5
ユーザーの資産管理(Asset)というEntityでは以下のように組み立てることができます。
asset = Asset(
id: uuid4(),
user_id: user.id,
asset_type: AssetCategory("Stock"),
asset: StockCode("APPL", "XNAS"),
volume: StockVolume(current_price=50, share=20, currency="USD").calculate(),
purchased_at: PurchasedDate.Parse(date)
)
Value ObjectはStockCodeやStockVolumeにあたりますが、単純な文字列や数値を指定するよりかなり意味が明白になったと思います。
Value Objectにはロジックも入れているので、ParseやConvertToForeignCurrentyなどのロジックも同梱できます。
Value Objectを使う側として「Entity」に言及しましたが、これは一意のidを持ち、状態が変更されるライフサイクルを持つようなモデルに対して用います。
先程の例でいえばAssetがその例になります。
StockCodeなどは一般的な値であるので自前でデータベースに保存することもないでしょうが、ユーザーが購入した資産を管理する時はそれを追跡する必要もあるのでidの割り振りが必須になります。ここで、Entityを用いたわけです。
しかしながら、Entityも必ずしも単体で用いられるわけではありません。
AggregatesがEntityを包含するものとして用いられ、複数のEntityやValue Objectを持つこともあり得ます。
AggregatesはEntityの上位概念ですが、実体としてはEntityの集まりです。
これはデータの一貫性を保障し、その方法は二つあります。
まずAggregates内の状態を変更できるのはAggregates内のロジックであって、他からの不用意な干渉を許しません。もし外からの変更を許すのであれば、公開メソッドを準備して一貫性に対する責任をAggregatesが明確に負うようにします。
次に、トランザクションの境界と対応してデータの一貫性を保障します。あるトランザクションにおいて、データの変更が行われるのはただ一つのAggregatesでなければなりません。もし他のAggregatesまで変更しようとすれば、分散トランザクションが暗黙の裡に行われてデータの不整合のリスクがあります。トランザクションと対応させることで、Aggregates内の処理はすべてが成功したか、失敗したかの二通りだけが結果として表れることになり、Rollebackがやりやすくなります。
Aggregatesは複数のEntityがある場合があると言いましたが、これは必ず外からの操作を受け付ける(窓役となる)一つのRoot Entityを頂点として他のEntityが従う階層構造になる必要があります。窓役は一つであって、他のEntityはAggregatesの中からのアクセスのみ許容します。
なお、Aggregatesに含まれるEntityは数が多くなるとトランザクションの単位が大きくなるので、関連性が強くデータの一貫性が必要なEntityに限定するべきでしょう。
Domain Serviceは、複数のAggregatesに跨って行われるべき業務ロジックを実行するために用いられます。
ただ、一つのAggregatesにはただ一つのトランザクションが対応する必要があるため、Domain Serviceはトランザクションを跨ぐこと(更新系処理)はするべきではありません。Aggregates内のデータを参照して計算を実行する処理なくらいのものでしょう。
3-2. ドメインモデルの使いどころ
先述した通り、ドメインモデルは複雑で重要な「中核的な業務領域」の実装において用いられるべきです。
単純なCRUDやETL処理においては、複雑な業務ロジックを伴う可能性が低く、アクティブレコードでよい場合が多いです。
アクティブレコードとは、ユーザーから受け取った値をデータベースの構造に合わせる形でマッピングする役割を負うものです。データベースの構造が複雑な場合に役に立ちます。
例えば、単にユーザーアカウントを管理する場合であれば、複雑なドメインロジックは想定されないので、これはアクティブレコードで充分でしょう。
ただ、もし資産管理システムを作るとして、資産のリスクやパフォーマンスを評価するロジックが必要になるとき、このロジックが複雑になることは目に見えているのでロジックとデータを合わせたドメインモデルを採用することになるでしょう。
Ref:ドメインイベント
先述した通り、Aggregatesは一つのトランザクションに対応しているので、トランザクションを跨った処理はできません。
しかし、通常Aggregatesはリポジトリパターンを利用して永続化(データベースに保存)されるわけですが、リポジトリを跨って処理したい場合があります。
例えば、Userが削除ないしnon-activeになった場合、Userに紐づけられたデータは削除ないしnon-activeにしなければなりません。
しかし、Userが削除されたからと言って、関連されたデータもすぐに削除しなければならないわけではありません。
このように、一つのAggregatesの中で行われた処理に対して
1.ユーザーへのメッセージ送信など外部との操作が必要になる
2.他のAggregatesにデータ変更を反映させたい
など副作用が起こる場面において、ドメインイベントは実装されます。
書籍ではイベント履歴式ドメインモデル(この記事では詳細を省きます)が紹介されてあるためか、Aggregatesを縦断するためのドメインイベントの実装方法については詳細には紹介されていませんでした。
即時的な整合が求められるなら一つのAggregatesにすればよいので、複数のAggregatesに跨るには結果整合だけがあればよいと考えると、ドメインイベントの非同期なディスパッチを行うことになるでしょう。
具体的にはOutBoxパターンを利用し、メッセージリレー関数がドメインイベントを監視して、イベントの追加に合わせてメッセージを発出し、その先(メッセージブローカー)でイベントハンドラーを実行します。
Aggregatesの原則を守るのは想像より難しいかもしれません。
ドメインイベントを実装するためだけに追加的なメッセージ管理システムを構築しなければならない場合、割に合わない感じもします。
私は自分のプロジェクトでは、複数のトランザクションに跨る処理をUnit of Workパターンで実装したのですが、こんな風に楽に処理しようとするとAggregatesの境界があいまいになります。
書籍を読んでいて、イベント履歴式ドメインモデルを使わないのであればここの実装コストが一番高いので、妥協の対象になりうるとも感じました。
4. おわりに
書籍では、「DDDの全てを実装するのがDDDであるわけではない」とありました。
実際、厳密なDDDの利用は結構難しそうに感じます。業務エキスパートと「同じ言葉」を使うこと戦略や、Value ObjectとAggregatesなどの戦術はかなり有用であると感じたので、部分的にでも使っていきたいなと思いました。



