第12章:集約ルート(入口は1つ)👑🚪
12.1 この章でできるようになること🎯✨
この章のゴールはこれ👇 **「集約は“入口(ルート)を1つ”にして、更新をそこに集める」**を、C#で手を動かしながら体に染み込ませることです💪😊
できるようになること👇
- ✅ 「外から触っていいのはココだけ!」という入口を作れる
- ✅ 集約の**ルール(不変条件)**をルートに集めて守れる🔐
- ✅ “勝手に壊されない”安全なモデルになる🛡️✨
12.2 集約ルートってなに?👑(超やさしく)

**集約(Aggregate)**は「整合性(ルール)を守るためのまとまり」でした🌳 その中で、**集約ルート(Aggregate Root)**は…
外部からの“入口”になる代表者 👑🚪 外からの更新は、必ずこの人を通す!
- Order(注文) ← これが集約ルート👑
イメージ👇(カフェ注文の例☕️)
-
Order(注文) ← これが集約ルート👑
- OrderItem(注文明細)
- 料金や数量などのルール
- 状態(未確定/確定…)
外側のコードは OrderItem を直接いじらない 🙅♀️ Order のメソッド経由だけで更新します✅
12.3 なぜ「入口は1つ」がそんなに大事?🤔💡

① ルールが散らばると、すぐ破綻する😵💫
例えば「注文確定後は明細を追加できない」ってルールがあるのに…
- ある場所では
order.Confirm()を通して守ってる - 別の場所では
order.Items.Add(...)で勝手に追加できちゃう
こうなると、ルールが“たまたま守られる”だけになって事故ります🚑💥
不変条件(Invariants)は「必ず守るルール」🔐 ルートが責任を持つと決めると、設計が一気に安定します😊
③ 変更点が追いやすくなる👀✨
更新がルートに集まると👇
- バグ調査が楽
- テストが楽
- 将来の仕様変更も楽
「入口は1つ」は、未来の自分を助ける魔法です🧙♀️✨
12.4 ルール3つだけ覚えよう📌(集約ルートの鉄板)
ルールA:外からの更新は “必ずルート経由” 🚪✅
- 外部コードが
OrderItemを直接setしたりList.Addしたりしない🙅♀️ - 代わりに
order.AddItem(...)みたいな ルートのメソッドを呼ぶ✨
ルールB:子(内部)は外に “丸出し” にしない🫣🚫
public List<OrderItem> Items { get; set; }← これは危険⚠️- 外に出すなら
IReadOnlyCollectionなど “読めるだけ” にする📖
ルールC:ルートが “ルールを守らせる” 🔐👑
-
ルートのメソッドの中で
- ガード節(早めに弾く)
- 状態チェック
- 数量・金額チェック を行って、壊れない更新だけ通す✨
12.5 例題ドメイン(カフェ注文)☕️🍰
登場人物🧑🍳
- Order(注文)👑 ← 集約ルート
- OrderItem(注文明細)
- OrderStatus(状態:Draft/Confirmed など)🚦
- Money(金額)💰(ValueObject)
この集約で守りたいルール(例)🔐
- ✅ 明細は数量が 1 以上
- ✅ 注文が確定したら、明細の追加・削除は禁止
- ✅ 同じ商品は「明細を増やす」じゃなく「数量を足す」でもOK(方針で決める)
この「守りたいルール」を、Order(ルート)に集めるのが今日の主役です👑✨
12.6 C#で実装してみよう🛠️✨(安全な集約ルート)
12.6.1 ValueObject(Money)💰
「金額」を decimal で直置きすると事故りやすいので、まず包みます📦✨
public readonly record struct Money(decimal Amount)
{
public static Money Zero => new(0m);
public Money
{
if (Amount < 0m)
throw new ArgumentOutOfRangeException(nameof(Amount), "金額はマイナスにできません。");
}
public static Money operator +(Money a, Money b) => new(a.Amount + b.Amount);
public override string ToString() => $"{Amount:N0}円";
}
ポイント😊
record structにして “値” として扱いやすくする✨- 生成時にガードして、不正な状態を作らない🔐
12.6.2 子(OrderItem)は外から勝手にいじれないようにする🧾🚫
public sealed class OrderItem
{
public Guid Id { get; }
public string ProductName { get; }
public Money UnitPrice { get; }
public int Quantity { get; private set; }
internal OrderItem(Guid id, string productName, Money unitPrice, int quantity)
{
if (string.IsNullOrWhiteSpace(productName))
throw new ArgumentException("商品名は必須です。", nameof(productName));
if (quantity <= 0)
throw new ArgumentOutOfRangeException(nameof(quantity), "数量は1以上です。");
Id = id;
ProductName = productName;
UnitPrice = unitPrice;
Quantity = quantity;
}
internal void IncreaseQuantity(int add)
{
if (add <= 0) throw new ArgumentOutOfRangeException(nameof(add));
Quantity += add;
}
public Money Subtotal => new(UnitPrice.Amount * Quantity);
}
ポイント😊
internalコンストラクタ&メソッドにして、集約の外から触れないようにする🚫- 「更新はルートからだけ」に寄せやすい✨
12.6.3 ルート(Order)が入口になる👑🚪
public enum OrderStatus
{
Draft = 0,
Confirmed = 1
}
public sealed class Order
{
private readonly List<OrderItem> _items = new();
public Guid Id { get; }
public OrderStatus Status { get; private set; } = OrderStatus.Draft;
// 外には“読めるだけ”で公開📖
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
private Order(Guid id)
{
Id = id;
}
public static Order CreateNew() => new(Guid.NewGuid());
public void AddItem(string productName, Money unitPrice, int quantity)
{
EnsureDraft(); // ✅ 状態ルールを守る
if (quantity <= 0)
throw new ArgumentOutOfRangeException(nameof(quantity), "数量は1以上です。");
// 例:同じ商品は数量を足す方針にしてみる😊
var existing = _items.FirstOrDefault(x => x.ProductName == productName && x.UnitPrice == unitPrice);
if (existing is not null)
{
existing.IncreaseQuantity(quantity);
return;
}
var item = new OrderItem(Guid.NewGuid(), productName, unitPrice, quantity);
_items.Add(item);
}
public void RemoveItem(Guid orderItemId)
{
EnsureDraft();
var target = _items.FirstOrDefault(x => x.Id == orderItemId);
if (target is null) return;
_items.Remove(target);
}
public void Confirm()
{
EnsureDraft();
if (_items.Count == 0)
throw new InvalidOperationException("明細が0件の注文は確定できません。");
Status = OrderStatus.Confirmed;
}
public Money Total => _items.Aggregate(Money.Zero, (acc, item) => acc + item.Subtotal);
private void EnsureDraft()
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("確定後の注文は変更できません。");
}
}
ここが超重要👑✨
ItemsはIReadOnlyCollectionで公開して、外から Add/Remove できない📖🚫- 更新は
AddItem / RemoveItem / Confirmなど ルートのメソッドに閉じ込める🔒 EnsureDraft()で 不変条件をガードしてる🛡️
12.7 “やっちゃダメ”な例(事故るやつ)🚑💥
ダメ例①:Items を public List にする🙅♀️
public List<OrderItem> Items { get; set; } = new();
これ、外側のコードが👇こうできちゃいます…
order.Items.Add(new OrderItem(...)); // ルートを無視して侵入💥
order.Items.Clear(); // 全消し💥
→ ルールが壊れても気づきにくい😱
ダメ例②:OrderItem の setter を public にする🙅♀️
public int Quantity { get; set; }
外側が勝手に👇
Quantity = -999;とか- 確定後に数量変更とか できちゃう…🫠
→ private set / internal / ルートのメソッド経由に寄せよう✅✨
12.8 “集約ルート経由”でアプリ層がどう書ける?🎬✨
アプリ層(ユースケース)から見ると、理想はこう👇
public sealed class OrderAppService
{
private readonly IOrderRepository _repo;
public OrderAppService(IOrderRepository repo)
{
_repo = repo;
}
public async Task AddItemAsync(Guid orderId, string name, decimal price, int qty)
{
var order = await _repo.GetAsync(orderId);
order.AddItem(name, new Money(price), qty);
await _repo.SaveAsync(order);
}
public async Task ConfirmAsync(Guid orderId)
{
var order = await _repo.GetAsync(orderId);
order.Confirm();
await _repo.SaveAsync(order);
}
}
public interface IOrderRepository
{
Task<Order> GetAsync(Guid id);
Task SaveAsync(Order order);
}
いいところ😊
- アプリ層は「何をしたいか」だけ書く🎬
- ルールはドメイン(Order)が守る🔐
- “勝手に壊れる道”がなくなる🛡️
12.9 Copilot / Codex 活用メモ🤖✨(この章で効く使い方)
✅ 目的:ルートのメソッド設計を“会話で固める”
おすすめプロンプト例👇(そのまま貼ってOK)
- 「Order を集約ルートにして、外部から OrderItem を直接変更できない設計にしたい。AddItem/RemoveItem/Confirm を用意して、不変条件(確定後変更禁止、数量1以上、明細0件確定禁止)を守る C# コード案を出して」🤖✨
- 「public setter を避けて、読み取り専用コレクションで公開する実装にして」📖✅
- 「上の Order の振る舞いに対する単体テストを xUnit で3本書いて」🧪✨
✅ チェック質問(AIの出力を採点するやつ)📝
- 「外側が
Items.Addできない?」👀 - 「確定後に変更できない?」🚦
- 「不正な状態を “作れない” ようにしてる?」🔐
12.10 ミニ演習(3本)✍️🎀
演習1:危険な公開を直そう🛠️
次のコード、どこが危ない?どう直す?😵💫
public class Order
{
public List<OrderItem> Items { get; set; } = new();
public void Confirm() { /* 省略 */ }
}
✅ 直す方向性
ItemsをIReadOnlyCollectionにする- 内部は
private readonly List<OrderItem> _itemsで持つ
演習2:ルール追加「最大10品まで」🧾🔟
注文は明細が 最大10件まで、というルールを追加したい!
✅ ヒント
AddItemの中でif (_items.Count >= 10) throw ...- “同一商品は数量加算”方針なら、件数の数え方も考えよう😊
演習3:確定後に数量変更したくなったら?🤔
「確定後の修正」要件が出たとき、つい Status を無視して変更したくなる…😇
✅ ここで大事な考え方
-
“確定後変更”は 別ユースケースとして扱うことが多い🎬
-
例:
- Confirmed をいったん Cancel して作り直す?
- 変更申請(RequestChange)という別の流れにする?
-
ルートに“抜け道”を作らないのが勝ち🏆✨
12.11 まとめ🌸✨
- 集約ルートは 外部からの入口 👑🚪
- 更新は ルートのメソッドに閉じ込める 🔒
- 子(内部)は 丸出しにしない(読めるだけ📖)
- ルートが 不変条件を守る責任者 🛡️🔐
ちなみに、C# 14 は .NET 10 SDK / Visual Studio 2026 で利用できるので、今回みたいな “安全なモデル設計” を最新環境でそのまま試せます😊✨ (Microsoft Learn) また、.NET 10 は 2025年11月リリースのLTSとして運用され、2026年1月にも更新が提供されています🧰✨ (マイクロソフトサポート)