第19章:集約① “一緒に守る範囲”って何?(超やさしい)📦🌸
まずは「今日できるようになること」🎯✨
- 「集約(Aggregate)」って結局なに?を 自分の言葉で説明できる💬
- カフェ注文アプリの例で、**Orderの“守る範囲”**を決められる☕️🧾
- **集約ルート(入口)**を通さない更新を禁止する、基本の形が書ける🚪🔒
🌟いまどきの最新版メモ(教材の前提が“古くならない”ように)🧭✨
- **.NET 10 は LTS(長期サポート)**として 2025/11/11 に公開(~2028/11/10 までサポート)📌 (Microsoft for Developers)
- C# 14 は最新のC#リリース(.NET 10 でサポート)🆕 (Microsoft Learn)
- Visual Studio 2026 は 2026/1/20 に 18.2.1 のリリースノートが出てるよ🛠️ (Microsoft Learn)
(この章の本題はDDDなので、ここは“チラ見”でOKだよ〜👀✨)
1) 集約ってなに?まずはイメージから📦🍱

集約(Aggregate)は、ひとことで言うと…
**「まとめて扱う“ひとかたまり”」**だよ😊 たとえば **注文(Order)**って、単体じゃなくて 注文明細(OrderLine) がくっついてるよね?
- 注文だけ保存して、明細は保存しない…ってできない😵💫
- 明細だけ増えて、注文が変な状態になる…も困る😱
だから 「注文+明細」をセットで整合性を守る箱として扱うのが、集約の考え方📦✨ (Martin Fowlerも、Order と Line-items をまとめて扱う例を挙げてるよ)(martinfowler.com)
2) 集約が必要な理由(やさしく3つ)🧠✨
理由①:ルール(不変条件)を“守りきる”ため🔒
「注文は確定したら明細をいじれない」みたいなルールって、Order と OrderLine にまたがるよね? こういう またがるルールを守るために「ひとかたまり」を作るのが集約📦✨
理由②:「同じトランザクションで守る範囲」を決めるため💾⚡
集約はよく 整合性(consistency)の境界って呼ばれるよ🌸 つまり「この箱の中は、更新が終わった瞬間に必ず正しい状態になっててね」って約束🤝 (集約ルートが不変条件を守る“入口(ゲート)”になる、という説明がMicrosoftのDDD資料にもあるよ)(Microsoft Learn)
理由③:ぐちゃぐちゃ参照を防いで、保守をラクにする🧹✨
みんなが OrderLine を勝手にいじり始めると、 「どこでルール守ってるの…?」って迷子になりがち😵💫 入口を1つにして、そこからしか更新できないようにすると安心🍀
3) 超重要ワード3点セット📌💡
✅ 集約(Aggregate)
- まとめて 整合性を守るオブジェクトの集合📦
✅ 集約ルート(Aggregate Root)
- 集約の代表(入口)🚪✨
- 外部から触っていいのは基本これだけ(中の子に直接触らせない)
✅ 境界(Boundary)
- 箱の内側と外側の線引き🧱
- 箱の外の整合性は、この箱は知らない(必要なら連携する)
4) カフェ注文アプリで:どこを集約にする?☕️🧾
🎯まず結論:この章ではこう置くのが分かりやすい!
- Order(注文)=集約ルート🧾🚪
- OrderLine(注文明細)=Orderの内側📄
- Money / Quantity / OrderId など=VO💎
- Product / Customer は “別集約” として扱い、ID参照にする🆔✨
🙅♀️やりがちなNG:Customerオブジェクトを丸ごとOrderに入れる
「Customerも必要だから入れちゃえ〜」ってやると…
- 注文の保存で顧客まで一緒に更新したくなる
- 顧客の住所変更が、注文の整合性に巻き込まれる
- 集約が肥大化して重くなる😵💫
なのでこの章では、Orderはこう持つのが安全寄り👇
CustomerIdとかCustomerEmail(必要最小限)だけ持つ🆔📧
5) いちばん大事:ルールを“言葉で”書こう✍️🌸
コードより先に、まず文章でOK!✨ (この文章が、そのまま メソッドや テスト名になるよ🧪)
たとえば Order 集約のルール案👇
- 注文は 明細が1つ以上ないと確定できない🧾➕
- 注文が Confirmed になったら、明細の追加・変更・削除は禁止🚫
- 金額の合計は 明細の合計から計算される(勝手に外から設定しない)💰
- 明細の数量は 1以上📦
- 通貨が違うMoneyは 足せない💱🚫(Money側のルールでも守る)
この「1〜5」を守る箱が Order集約だよ📦✨
6) 実装のコツ:「入口」を1個にする🚪🔒

✅狙い
- 外から
Order.Lines.Add(...)みたいに触れないようにする🙅♀️ - 更新は
Order.AddLine(...)のような 意図が読めるメソッドに寄せる📘✨
✅形(最小サンプル)🧩
(細部は第20章で育てるので、ここは“型”をつかむだけでOK☺️)
public sealed class Order
{
private readonly List<OrderLine> _lines = new();
public OrderId Id { get; }
public OrderStatus Status { get; private set; } = OrderStatus.Draft;
// 外には「読めるだけ」で渡す(Listをそのまま返さない!)
public IReadOnlyList<OrderLine> Lines => _lines;
public Order(OrderId id)
{
Id = id;
}
public Result AddLine(ProductId productId, Quantity quantity, Money unitPrice)
{
if (Status != OrderStatus.Draft)
return Result.Fail("確定後は明細を変更できません🥺");
// 例:同じ商品はまとめたい、みたいなルールもここに置けるよ
_lines.Add(new OrderLine(OrderLineId.New(), productId, quantity, unitPrice));
return Result.Ok();
}
public Result Confirm()
{
if (_lines.Count == 0)
return Result.Fail("明細が0件の注文は確定できません🥺");
Status = OrderStatus.Confirmed;
return Result.Ok();
}
}
public sealed class OrderLine
{
public OrderLineId Id { get; }
public ProductId ProductId { get; }
public Quantity Quantity { get; }
public Money UnitPrice { get; }
internal OrderLine(OrderLineId id, ProductId productId, Quantity quantity, Money unitPrice)
{
Id = id;
ProductId = productId;
Quantity = quantity;
UnitPrice = unitPrice;
}
}
public enum OrderStatus { Draft, Confirmed, Canceled }
public readonly record struct Result(bool IsSuccess, string? Error)
{
public static Result Ok() => new(true, null);
public static Result Fail(string error) => new(false, error);
}
ここがポイントだよ〜💡✨
Linesは IReadOnlyList で返す(外からAddできない)🔒- 明細生成は
Order.AddLine()だけに寄せる(入口が1つ)🚪 - ルールは Orderの中で判断する(散らさない)🧠✨
(“集約ルートがルールを守るゲートになる”という説明はMicrosoftのDDD資料でも強調されてるよ)(Microsoft Learn)
7) テストで「箱のルール」を固定しよう🧪🧷
集約は テストと相性がめちゃ良いよ✨ なぜなら「ルールが箱に集まる」から!
using Xunit;
public class OrderTests
{
[Fact]
public void Confirm_without_lines_should_fail()
{
var order = new Order(new OrderId(Guid.NewGuid()));
var result = order.Confirm();
Assert.False(result.IsSuccess);
}
[Fact]
public void AddLine_after_confirm_should_fail()
{
var order = new Order(new OrderId(Guid.NewGuid()));
order.AddLine(new ProductId("COFFEE"), Quantity.Create(1), Money.Jpy(450));
order.Confirm();
var result = order.AddLine(new ProductId("TEA"), Quantity.Create(1), Money.Jpy(400));
Assert.False(result.IsSuccess);
}
}
※ Quantity.Create や Money.Jpy は、前の章で作ったVOがある想定だよ💎☕️
(無ければ “仮の簡易版” でOK!)
8) よくある事故あるある😱→こう回避しよ🛡️✨
❌事故①:public List<OrderLine> Lines { get; set; }
外から勝手に Lines.Clear() とかされて、ルール崩壊💥
✅ IReadOnlyListで返すのが安全🔒
❌事故②:OrderLineを外から直接更新できる
「Confirmed後に数量書き換え」みたいなのが起きる😵💫 ✅ 変更メソッドはOrderに寄せる(OrderLineは内部用)🚪
❌事故③:集約が巨大化する(全部入り弁当🍱)
Customer、Product、Inventory、Coupon…全部Orderに入れると地獄😇 ✅ 本当に“同一トランザクションで絶対守りたいルール”だけ箱に入れる📦✨
9) Copilot / Codex 活用(この章での使いどころ)🤖💞
✅おすすめプロンプト例(そのままコピペOK)📝✨
- 「Order集約の不変条件を5つ、日本語で箇条書きにして。カフェ注文アプリ想定☕️」
- 「OrderをAggregate Rootにして、外からOrderLineを直接変更できないC#コード例を出して」
- 「上のルールをxUnitテスト名(英語でも日本語でも)に落として」
- 「LinesをIReadOnlyListで公開しつつ、内部Listで管理する実装の注意点をレビューして」
⚠️注意:AIが作るコードは“動くけど設計が甘い”こともあるから、 「外から変更できない?」だけは目でチェックしてね👀🔒
10) ミニ演習(10〜15分)⏱️🌸
演習A:境界線引きクイズ✂️🧠
次のうち、Order集約の「内側」に入れたいのはどれ?理由も一言で!✍️
- OrderLine
- Money
- Quantity
- Customer(オブジェクト丸ごと)
- CustomerId
- Product(オブジェクト丸ごと)
- ProductId
演習B:ルールを3つだけ文章にする📌
- 「確定後は〜できない」
- 「〜が0件なら確定できない」
- 「合計金額は〜から計算する」
演習C:コードを1か所だけ直して安全にする🔧
LinesがList<OrderLine>になってたらIReadOnlyList<OrderLine>に変更する🔒✨
まとめ(1分)🪄✨
- 集約= **“一緒に整合性を守る箱”**📦
- 集約ルート= **更新の入口(ゲート)**🚪🔒
- Order + OrderLine は、集約の例として超わかりやすい🧾✨
次の第20章では、この **Order集約を“それっぽく完成”**させていくよ〜!☕️🧾📦💖