Skip to main content

第59章:集約のアンチパターン集(よくやる)😂⚠️

今日は「集約(Aggregate)が“DDDっぽいだけ”になっちゃう罠」を、あるある+直し方で一気に整理するよ〜🧸✨ (※2026-02-07時点の最新:TypeScript の最新版は 5.9.3 だよ📌 (GitHub))


0. 今日のゴール🎯✨

学び終わったら、こんなことができるようになるよ💪💖

  • 「これ…集約が太り始めてるかも😇」って匂いを嗅げる👃
  • “やりがち設計”を見つけて、最小の修正で健康体に戻せる🩺
  • テストで「もう戻らない」を固められる🧪🔒
  • AIにレビューさせて、見落としを減らせる🤖✅

1. まず30秒だけ復習⏱️🏯

集約ってざっくり言うと、

  • 守りたい不変条件(ルール)を、1つの“整合性の境界”に閉じ込めるもの🔒
  • 外から触れる入口は Aggregate Root 1つにする(門番👑🚪)
  • トランザクションは基本“集約の境界をまたがない”(またぐとしんどい) (martinfowler.com)
  • ルートが責務を持ち、子を勝手にいじれないようにして整合性を守る (martinfowler.com)

ここがブレると、今日のアンチパターン祭りが始まる…😂🔥


2. 集約アンチパターン10連発😂⚠️(症状→事故→治し方)

以下、全部「カフェ注文(Order集約)」でよく起きるやつ☕🧾


❶ 巨大集約(なんでも Order に詰める)🐘📦

あるある症状

  • Order の中に、支払い、顧客、メニュー、在庫、クーポン…全部いる😇
  • 変更のたびに関係ないところが壊れる💥

起きる事故

  • 1回の更新が重い(保存も読み込みも)🐢
  • ちょっとした変更で「全部同時に整合性」を要求しがち → 詰む😵‍💫
  • “集約の境界をまたがない”原則が崩れて、設計が泥沼化 (martinfowler.com)

治し方(コツ)

  • 同時に守るべき不変条件はどれ?」で境界を決める🔒
  • 他集約は ID参照が基本(オブジェクト参照しない) (martinfowler.com)
  • どうしても跨ぐなら「イベント」や「アプリ層の調停」で扱う(第91章以降の布石⚡)

❷ “配列をそのまま返す”問題(外から中身が壊される)🧨🧺

あるある症状

  • order.lines.push(...) が外からできちゃう
// ❌ NG:外から直接いじれてしまう
export class Order {
lines: OrderLine[] = [];
}

起きる事故

  • ルール無視の追加・削除が可能 → 不変条件が崩壊💥
  • 「テストでは通ったのに本番で壊れた」が起きやすい😇

治し方

  • コレクションは private / # に閉じ込める🔒
  • 外に出すなら **読み取り専用(ReadOnly)**にする🧊
export class Order {
#lines: OrderLine[] = [];

getLines(): ReadonlyArray<OrderLine> {
return this.#lines; // OrderLineが不変ならOK(VO寄りなら特に👍)
}

addLine(line: OrderLine) {
// ここで不変条件チェック✅
this.#lines.push(line);
}
}

🧪テスト観点

  • getLines() で取得した配列をいじっても、Orderの中身が壊れない(or いじれない)
  • addLine 以外で増えない

❸ setter祭り(いつでも何でも変更できる)🎉🚫

あるある症状

  • setStatus(Paid) とか、setTotal(999) とかできちゃう
// ❌ NG:ルールの門番がいない
order.setStatus("Paid");
order.setTotal(999);

起きる事故

  • 「支払い後は明細変更不可」みたいなルールが守れない😇
  • 仕様が増えるほど if が散らばって地獄🔥

治し方

  • “やりたいこと”メソッドで表現する(意図が残る)🧠✨
  • ルールは集約内でガードする🔒(ルートが整合性を守る考え方) (martinfowler.com)
export class Order {
#status: "Draft" | "Confirmed" | "Paid" = "Draft";

confirm() {
if (this.#status !== "Draft") throw new Error("確定できません");
this.#status = "Confirmed";
}

pay() {
if (this.#status !== "Confirmed") throw new Error("支払いできません");
this.#status = "Paid";
}
}

❹ 集約ルートをスキップ(子Entityを直で更新)🏃‍♀️💨

あるある症状

  • OrderLineRepository を作って、明細だけ直接更新してる
  • DB都合で “部分更新” したくなるやつ😇

起きる事故

  • ルートを通らない更新で、不変条件が破壊される💥
  • 「いつの間にか合計金額が合わない」みたいな幽霊バグ👻

治し方

  • 基本は「Repositoryは集約ルート単位
  • 子の更新は order.changeQuantity(lineId, qty) みたいにルート経由で行う

❺ 他集約を“オブジェクト参照でベタ持ち”🔗🕸️

あるある症状

  • Order が MenuItem オブジェクトを丸ごと保持してる
  • CustomerPayment も全部参照…(オブジェクトグラフ地獄)😵‍💫

起きる事故

  • 読み込みが重い・循環参照・差分が追えない💥
  • 「ついで更新」したくなって境界崩壊🔥
  • 参照はIDが基本、という原則から外れる (martinfowler.com)

治し方

  • 他集約は ID参照MenuItemId だけ持つ)🪪✨
  • 必要ならアプリ層で取得して使う(ドメインに押し込めない)

❻ 集約内で外部I/O(API叩く・DB触る・今の時刻を直で取る)🌩️🔌

あるある症状

  • pay() の中で決済API呼んでる
  • Date.now() 直呼びで期限判定してる

起きる事故

  • テストが地獄(モックだらけ)🧪🫠
  • ドメインが “外の都合” に侵食されて壊れやすい

治し方

  • 集約は「ルールと状態」を中心に保つ🧊
  • 外部連携は アプリ層(or ドメインサービス)で調停
  • “今” は注入(Clock)で扱う(第86章の流れにも繋がる⏰)

❼ 不変条件がアプリ層に漏れる(ユースケースにifが増殖)🧟‍♀️🧯

あるある症状

  • PlaceOrderPayOrder の中に 「支払い済みなら弾く」みたいなルールが散らばる

起きる事故

  • 1箇所直したつもりが、別ユースケースだけ古いまま😇
  • 仕様追加で “漏れ” が増える

治し方

  • ルールは集約のメソッドへ(門番はドメイン)🔒
  • アプリ層は「手順(オーケストレーション)」に徹する🎬

❽ “貧血ドメインモデル”(集約がただのDTO)🧟‍♂️📄

あるある症状

  • Order はプロパティだけ
  • ロジックは全部 OrderService にある

これ、Martin Fowler が「Anemic Domain Model(貧血モデル)」としてアンチパターン扱いしてるやつだよ⚠️

起きる事故

  • ルールが散らばる
  • “どこが正しいの?”が分からなくなる😵‍💫

治し方

  • 「データ+ルール」を同居させる(集約が主役に戻る)👑
  • Service は “調停” や “複数集約をまたぐ手順” に限定

❾ 「整合性=全部同期で完璧に」思考(分散トランザクション欲)😇🧨

あるある症状

  • 注文と支払いと在庫を 1トランザクションで全部確定したい
  • でも現実は外部連携がある…💳🌍

起きる事故

  • 例外・リトライ・タイムアウトで沼る
  • “境界をまたがない”設計が崩れる (martinfowler.com)

治し方

  • まずは「集約内は強整合」+「集約間はID参照」へ戻す🧊
  • 必要なら イベント駆動(第91章〜)で段階的に整合させる⚡

❿ “便利メソッド盛りすぎ”で集約が神クラス化👼📦

あるある症状

  • order.exportCsv()
  • order.toDisplayDto()
  • order.save() みたいな「便利そうなもの」を全部 Order に入れちゃう

起きる事故

  • ドメインが “表示” “永続化” “外部出力” に汚染される🧼💥
  • 変更理由が増えて壊れやすい

治し方

  • 変換はDTOマッパー、保存はRepository、表示はUI/Presenterへ分離🧩✨
  • 集約は ルールと状態の中心に戻す🏯

3. 臭い診断チェックリスト👃📝(YESが多いほど危険⚠️)

  • ルート以外を直接更新できる(子を直で触れる)
  • public setter がある / setStatus がある
  • コレクションをそのまま返している
  • 集約の中でAPI/DB/Clock直呼びしてる
  • 他集約をオブジェクトで持ってる(IDじゃない)
  • ルールがユースケースに散らばってる
  • Order が“何でも屋”になってきた
  • 1つ変えると関係ないテストが落ちる
  • ちょっとした更新に大量ロードが必要
  • 「このルールどこに書いた?」が頻発する

4. “安全に”直す手順(おすすめ)🛠️🧪

  1. 不変条件を1行で書き出す🔒
  2. その不変条件が守れるように、入口をルートに集約🚪👑
  3. 外から触れてる箇所を潰す(setter削除・配列非公開)🧊
  4. 先にテストを書く(壊したくないルールから)🧪
  5. 他集約参照をIDへ寄せる(必要ならアプリ層で取得)🪪
  6. 外部I/Oはアプリ層へ退避🔌➡️🎬

5. AIレビューで“臭い”を自動で拾う🤖🔎(コピペ用)

✅ アンチパターン診断プロンプト

「Order集約コード」を貼って、これを投げてみてね👇

  • あなたはDDDのコードレビュアーです。
  • 次のTypeScriptコードを読み、Aggregateのアンチパターンを最大10個指摘してください。
  • 指摘は「症状 → なぜ危険 → 最小の修正案」の順で。
  • “集約境界をまたぐ更新” “setter/コレクション露出” “他集約のオブジェクト参照” “外部I/O混入” を優先的にチェックしてください。
  • 最後に、追加すべきテストケースも提案してください。

(AIの提案はそのまま採用せず、不変条件を守れるかだけは必ず自分でチェックね🧸🔒)


6. ちょい最新TSメモ(この章に効く)📌✨

  • TypeScript 5.9 では import defer など新要素が追加されてるよ(副作用の実行タイミングに関わるので、“ドメインに副作用を入れない”意識と相性◎) (TypeScript)
  • tsc --init の生成内容もアップデートされていて、noUncheckedSideEffectImports みたいな「副作用インポートを気づかせる」設定も出てるよ🧯 (TypeScript)
  • Node.js は “ProductionはLTS推奨” が明確で、2026-02-07時点だと v24がActive LTS、v22/v20がMaintenance LTS(参考) (Node.js)

まとめ🎀

集約アンチパターンの正体は、だいたいこの3つに集約されるよ👇

  • 入口が開きすぎ(setter/配列露出/ルートスキップ)🚪💥
  • 境界を無視(他集約を抱え込む/跨ぐ整合性を同期でやる)🧱🔥
  • 責務が混ざる(I/Oや変換や永続化がドメインに侵入)🧼🧨

次章(第60章)で「VO/Entity/Agg の核3点」をピカピカにまとめ直すから、ここで“罠回避スキル”を身につけておくと超ラクになるよ〜✨🏯🧡


必要なら、こみやんまの「今のOrder集約コード(短めでOK)」貼ってくれたら、上のチェックリストでどれが刺さってるか一緒に診断して、最小リファクタ案まで作るよ🧸🔍✨