ブログ記事
TypeScriptで学ぶDDDの実装:集約をIDで参照するという設計判断の意味
「ドメイン駆動設計(DDD)は概念が難解で、実際にどうコードに落とすかが分からない」——この壁に当たったことがある開発者は多い。山下祐也・著、増田亨・監修の『つくりながら学ぶ!ドメイン駆動設計 実践入門』は、オンライン書店の開発をTypeScriptで実装するという具体的なコードを通して、その「どうコードに落とすか」に答えを出そうとしている。
1. アーキテクチャの型よりもドメインを先に
クリーンアーキテクチャのフォルダ構成を整えてから開発を始める——この順序に問題がある、と本書は示唆する。
フォルダ構成の整合性はツールや規約が保証できる。しかしビジネスのルール(ドメイン)をどのクラスに持たせるかは、チームがドメインを理解しているかどうかで決まる。本書が最初に扱うのはドメインの定義であり、インフラの設定ではない。
「DDDは手順書ではなく、考え方」という立場が実装例の随所に表れている。
2. 集約を小さく保つ——ID参照という設計判断
DDDを学ぶ開発者が最初に直面する難問の一つが「集約(Aggregate)の境界をどこに引くか」だ。
ORMに慣れた開発者は、関連するオブジェクトをすべてネストして表現しがちだ。商品クラスが注文リストを持ち、注文クラスがレビューを持つ——という構造は直感的だが、問題を抱える。
- 商品を1件読み込む際に、関連する何百件もの注文データが一緒にロードされる
- 別々のユーザーが異なる変更を同じ集約に加えようとすると、トランザクション競合が起きやすくなる
本書の解決策は「他の集約への参照はIDで持つ」というルールだ。Order クラスは Product オブジェクトを直接持たず ProductId だけを持つ。必要な時に必要なデータだけ取得する。
この設計はコーディング規約ではなく、アーキテクチャ上の判断だ。後からマイクロサービスへ分割する際の境界になり、分散システムでの独立したデプロイを可能にする。
3. 値オブジェクトで「貧血モデル」を脱する
ゲッターとセッターだけを持つデータの入れ物クラスは「貧血モデル」と呼ばれる。ビジネスロジックを持たないため、利用側があちこちでバリデーションや計算を行うことになる。
値オブジェクトは逆のアプローチをとる。「商品名は1文字以上50文字以下でなければならない」というルールを ProductName クラスのコンストラクタで保証する。TypeScriptの静的型システムを利用して、不正な状態を「型として表現不可能」にする。
class ProductName {
private constructor(private readonly value: string) {}
static create(name: string): ProductName {
if (name.length < 1 || name.length > 50) {
throw new Error('商品名は1〜50文字で入力してください');
}
return new ProductName(name);
}
}
このクラスを string の代わりに引数型として使えば、コンパイル時に渡し間違いを検出できる。
4. 複数集約にまたがる整合性——Outboxパターン
集約を小さく分割すると、複数の集約を一度のトランザクションで更新する「即時整合性」を保ちにくくなる。
例えば、注文確定時に「注文の保存」と「メール通知の送信」の両方が必要な場合、DBへの保存は成功したが通知が失敗するケースへの対応が必要になる。
Outboxパターンはこの問題に対処する設計だ。アプリケーションはDBへの書き込みと同じトランザクション内に「送信すべきイベント」を専用テーブル(Outbox)へ記録する。別プロセスがそのテーブルを監視して非同期にメッセージを送信することで、「DBへの保存が成功したなら、いつかかならずイベントが発火する」という保証を作る。
難易度は高いが、本書はこのパターンをステップを追って実装例として示している。
DevBookPath のマップで確認する
この本の前後の読書順は、DevBookPath のグラフで確認できます。
本記事のリンクには Amazon アソシエイト等の広告が含まれる場合があります。リンク経由の購入で運営者に紹介料が支払われることがあります。
この記事を共有
この地図を共有