第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オブジェクトを丸ごと保持してる CustomerもPaymentも全部参照…(オブジェクトグラフ地獄)😵💫
起きる事故
- 読み込みが重い・循環参照・差分が追えない💥
- 「ついで更新」したくなって境界崩壊🔥
- 参照はIDが基本、という原則から外れる (martinfowler.com)
治し方
- 他集約は ID参照(
MenuItemIdだけ持つ)🪪✨ - 必要ならアプリ層で取得して使う(ドメインに押し込めない)
❻ 集約内で外部I/O(API叩く・DB触る・今の時刻を直で取る)🌩️🔌
あるある症状
pay()の中で決済API呼んでるDate.now()直呼びで期限判定してる
起きる事故
- テストが地獄(モックだらけ)🧪🫠
- ドメインが “外の都合” に侵食されて壊れやすい
治し方
- 集約は「ルールと状態」を中心に保つ🧊
- 外部連携は アプリ層(or ドメインサービス)で調停
- “今” は注入(Clock)で扱う(第86章の流れにも繋がる⏰)
❼ 不変条件がアプリ層に漏れる(ユースケースにifが増殖)🧟♀️🧯
あるある症状
PlaceOrderやPayOrderの中に 「支払い済みなら弾く」みたいなルールが散らばる
起きる事故
- 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行で書き出す🔒
- その不変条件が守れるように、入口をルートに集約🚪👑
- 外から触れてる箇所を潰す(setter削除・配列非公開)🧊
- 先にテストを書く(壊したくないルールから)🧪
- 他集約参照をIDへ寄せる(必要ならアプリ層で取得)🪪
- 外部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)」貼ってくれたら、上のチェックリストでどれが刺さってるか一緒に診断して、最小リファクタ案まで作るよ🧸🔍✨