この記事は BASE アドベントカレンダー 14日目の記事です。
はじめに
BASEのProduct Divにてバックエンドエンジニアをしているオリバです。
当該記事では、所属しているチームメンバーで「ドメイン駆動設計をはじめよう」の輪読会を実施したので、印象に残った内容やチーム内で議論したことを紹介しようと思います。
(画像はhttps://www.oreilly.co.jp/books/9784814400737/より引用)
背景と目的
2024年10月29日、私の所属するチームは「かんたん発送(日本郵便連携)App」(以下、かんたん発送App)をリリースしました。(※かんたん発送Appとは)
現在BASEでは、新規Appの開発プロジェクトにおいてDDD(ドメイン駆動設計)を推奨しており、かんたん発送Appもこのアプローチを採用しています。
DDDを採用するにあたって、チームメンバーでDDDの知見があるエンジニアがほとんどいなかったため(かくいう私も半年ぐらい)、「ドメイン駆動設計をはじめよう」でDDDに関してインプットをし、開発プロジェクトにアウトプットしていく目的で、この本の輪読会を実施することになりました。
事業活動を分析する
DDDを取り入れるべきか判断するため、事業活動の分析の重要性を説いており、3つの重要なワードが出てきます。
1. 中核の業務領域
競合他社との差別化を図る事業戦略の核心で、業務ロジックが複雑なものです。ここを外部に委託するのは、賢い選択ではありません。
2. 一般的な業務領域
競合他社と同じやり方で課題を解決する領域で、競争優位を生み出さないため、外部サービスを積極的に使います。業務ロジックの複雑さは小さいです。
3. 補完的な業務領域
中核の業務領域を補完する業務領域で、業務ロジックの複雑さは小さいです。一般的な業務領域との違いは、自社で開発するか外部サービスを使用するかどうかです。
事業分析で大事な視点は、競合他社との優位性と業務ロジックの複雑性です。
そこで、輪読会でかんたん発送Appがどの業務領域にあたるか話し合いました。
BASEでは、複数の機能をAppsという形で提供しており、かんたん発送Appを使用することで、日本郵便を使用した配送を行う際に、送り状の宛名書きが不要で、ショップオーナー様の発送時の負担を軽減することができ、かつ配送料金も全国一律利用しやすい価格帯となっております。
かんたん発送Appは、競合他社との優位性という観点では中核の業務領域に位置づけられます。しかし、業務ロジックの複雑性については、一部に複雑なロジックは含まれるものの、他の機能と比較すると比較的シンプルな構造となっています。そのため、中核業務とも補完業務とも位置づけらるという結論に至りました。
業務ロジックの複雑性は、他機能との比較やエンジニアの経験値によるものでもあるので、判断するのは難しいと感じました。
業務知識を発見し、事業の複雑さに立ち向かう
同じ言葉
ソフトウェア技術者がソフトウェアを設計、組み立てるために、「同じ言葉」を用いて意図の伝達と知識の共有を行います。
ソフトウェアが対象としている業務を表現するときに、プロジェクトメンバー全員が共通して使用する言葉を指します。本書のxxiページの訳語表を見ると、同じ言葉は従来の訳語で「ユビキタス言語」と呼ばれています。
かんたん発送Appでも同じ言葉を定義しドキュメントを用意しておりましたが、あまり利用されず最終的には形骸化されてしまいました。
原因としては、以下の2つが考えれられます。
- 同じ言葉は、事業活動を表現すると同時に、業務エキスパート(業務領域に最も詳しい人でドメインエキスパートともいう)の考えや発想を表現する際に使うものですが、かんたん発送Appでは業務エキスパートが不在でした。
- 業務エキスパートが不在だったので、ドメインモデリングをやる際も、エンジニア内で閉じてしまいました。
区切られた文脈
業務エキスパートによって業務内容の捉え方が異なる場合があり、同じ言葉を区切られた文脈で分解する必要があります。
ECサイトの例で考えると、エンジニアが販売部とやり取りする際に使用する「商品」という言葉と、配送部とやり取りする際に使用する「商品」という言葉が、それぞれ異なる意味を持っている場合、コミュニケーションの中で誤解が生じる可能性があります。販売部では「商品」が売値や在庫を指すのに対し、配送部では配送先や配送状況を指すことがあります。このように関係者ごとに異なる意味の「商品」を扱う必要があり、意味のズレが生じやすくなります。
そこで「区切られた文脈」を定義し、それぞれの文脈内で「商品」という言葉を統一することで、誤解を防ぎます。本書の xxi ページの訳語表を見ると、「区切られた文脈」は従来の訳語で「境界づけられたコンテキスト」と呼ばれています。 ※「区切られた文脈」の詳細については、こちらの記事をご参照ください。
本書では「業務領域は発見で、区切られた文脈は設計」と記されています。これは非常に納得感のある表現です。区切られた文脈はアーキテクチャと密接に関連しており、例えば以下のように設計判断に結びつきます。
- モジュラーモノリスを採用している場合:区切られた文脈ごとにモジュールを分割
- マイクロサービスを採用している場合:区切られた文脈ごとにサービスを分割
業務ロジックを実装する
業務ロジックを実装する方法として以下の3つがあります。
1. トランザクションスクリプト
トランザクションスクリプトは、データ操作の手続きを手順に沿って順次処理するスクリプトとして記述する設計手法です。主にデータのETLなど、業務ロジックが比較的単純で手続き的な操作を伴う処理に適しています。このアプローチは、業務ロジックが複雑でない場合に有効で、補完的な業務領域で使用されるケースが多いといえます。
この設計手法をイメージする例として、私は真っ先にAWS Lambdaを使用したコードが思い浮かびました。イベントハンドラーから入力値を受け取り、それを順次処理し、出力するという流れが、トランザクションスクリプトに非常に近い印象を受けます。(もちろん、AWS Lambdaを使用してオブジェクト指向設計やDDDを実現することも可能ですが、ここでは単純なトランザクションスクリプトのイメージとして取り上げています)
2. アクティブレコード
アクティブレコードは、トランザクションスクリプトと同様に単純な業務ロジックを実装する方法ですが、より複雑なデータ構造を扱える点が特徴です。この設計手法では、ORM(Object-Relational Mapping)を使用してデータベースとオブジェクトを直接対応させ、データの操作を効率的に行います。
例えば、Ruby on RailsのActive RecordやLaravelのEloquentなど、多くのWebフレームワークが標準機能としてアクティブレコードの仕組みを提供しています。これらを活用することで、開発者はデータベース操作を簡素化しつつ、業務ロジックを実装することが可能です。
BASEでは、社内向けにデータを管理するためのAdmin機能を提供しており、これらの業務領域ではアクティブレコードが利用されています。具体的には、Admin機能はCakePHPで構築されており、シンプルな補完業務領域に適した設計となっています。
3. ドメインモデル
ドメインモデルは複雑なロジックを実装する手段で、必要な部品は以下の3つです。
- 値オブジェクト
- 集約
- 業務サービス
かんたん発送Appは、業務領域が配送業務なのでBASEにおける中核業務に近く、またBASEの配送領域が今後「区切られた文脈」として新たに切り出される予定という背景から、ドメインモデルを採用しました。
また、チームでは集約の境界線の設定が難しいという話がよく上がっていました。本書によると、境界線の基準は「一貫性の強制」にあり、集約の状態変更は集約内部の業務ロジックのみが行えるとされています。そのため、複数の集約をまたぐトランザクションは実行できないことになります。
たとえば、バッチ処理では、複数の集約をまたいで1つのトランザクション内で状態を変更することがあります。結果整合性のトランザクションであれば、ドメインイベントを使用して集約外部に変更を伝播できます。しかし、強整合性が必要で集約をまたぐ場合は集約の設計自体を見直す必要があり、これは非常に難しい課題だと感じました。
設計を改善する
事業は常に進化していくため、一般業務が中核業務へ、または補完業務が中核業務へと変化することがあります。もちろん、その逆のパターンも起こり得ます。
アクティブレコードで実装されていた業務ロジックを、ドメインモデルに変化する場合どのように行えば良いか以下の手順が示されています。
- 値オブジェクトを見つける
- 値オブジェクトに関連する業務ロジックを見つけ、値オブジェクトのメソッドに移す
- トランザクションの境界(集約)を探す
- 状態を変更するメソッドを明確にするために、setterはprivateにする
- 集約ルートとなるオブジェクト(エンティティ)を探す
興味深いのは、エンティティを探すのが最後のステップとなっている点です。新規のドメインモデリングでは通常、集約ルートを先に決めてトップダウン式で進めますが、設計変更の場合は上記の手順に従って、まず値オブジェクトを特定し、最後に集約ルートを探すようボトムアップ式で進めていきます。
集約ルートを決めることは、外部に公開する唯一のエントリーポイントを決めることを意味します。そのため、まず値オブジェクトを見つけていき、最後に集約ルートを決めるというボトムアップ式が採用されているのだと推察します。
イベント駆動型アーキテクチャ
イベント駆動型アーキテクチャとは、システムのコンポーネント間でイベントメッセージを非同期でやり取りする技術方式です。
イベントには「イベント」(すでに起こった変化)と「コマンド」(これから実行すべき操作)の2種類があります。BASEでは主にイベントを使用しており、ドメインの状態変化やユースケースの実行時にイベントを発行します。
第6章で述べられているように、ドメインイベントは実際に発生した出来事を表現するため、その名前は必ず過去形で記述します。この原則を学んだことで、チーム内でもドメインイベントの命名は必ず過去形にするという意識が定着しました。
また、15章p.279 イベント通知にて、「イベント通知の内容は必要最低限です。イベントの購読者にイベントの発生を伝えることが唯一の目的です。イベントに反応するために必要な全ての情報をイベント通知に含めてはいけません。」という記述があります。この理由として、以下の2つが上げられています。
- 保護すべき情報を、メッセージ通信基盤に漏洩させないため
- メッセージが届いた時には、その内容が最新では無くなっている可能性がある
例えば、イベントでエンティティの状態を渡す際、エンティティ全体を渡すべきか、IDのみを渡すべきかについてチーム内で議論が生まれました。本書のルールに従うと、解決策は明確です。最新の状態が必要な場合はエンティティIDを渡し、受信側で最新のエンティティを取得すれば良く、特定時点の状態(スナップショット)が必要な場合はエンティティ全体を渡せば良いという結論に至りました。
おわりに
本書は、前半でDDDの戦略的な内容を扱い、後半で戦術的な内容を詳しく解説しており、非常に充実した内容でした。また、業務領域(中核、一般、補完)を常に意識するようになり、良い意識改革となりました。このタイミングで輪読会を実施できて本当に良かったです。
明日のBASEアドベントカレンダーは @miyachin の記事です、お楽しみに!
また、BASE では Web アプリケーションエンジニアを積極採用しています。興味持っていただいたら、ぜひご応募ください!