Skip to main content

第57章:Order集約を実装②:遷移とガード節🚦🛡️

この章は「不正操作を完全ブロックする」回だよ〜!✨ Order集約(Orderが集約ルート🏯👑)に、confirm/pay/cancel/fulfill を実装して、状態遷移のルールと**ガード節(早期チェック)**でガチガチに守ります💪💞

※最新動向メモ:現時点(2026-02-07)では TypeScript は 5.9 系が安定版で、6.0 は 2026-02-10 にBeta、2026-03-17 にFinal予定…という計画が公開されています📅✨ (GitHub) (なので、この章のサンプルは TS 5.9 前提でOKだよ〜!)


1) ゴール(この章の到達点)🎯✨

  • setStatus() みたいな “雑な変更” を 不可能にする🚫
  • confirm() / pay() / cancel() / fulfill()唯一の状態変更ルートになる🚪
  • ✅ どの操作も、先に ガード節で「やっちゃダメ」を即ブロック🛡️
  • ✅ 例外(ドメインエラー)メッセージが、後でUIに出せるくらい “人間にやさしい”👩‍🍳💬

2) まずは状態遷移を「仕様」として固定する🚦📌

カフェ注文ドメインの例だと、こんな感じが分かりやすいよ〜☕🧾

現在の状態できること次の状態
Draft(下書き)confirm / cancel / 明細編集Confirmed / Canceled
Confirmed(確定)pay / cancelPaid / Canceled
Paid(支払い済)fulfillFulfilled
Canceled(取消)何もできない-
Fulfilled(提供済)何もできない-

ここがDDDで超大事💡 「状態が違うと、できる操作が違う」=ドメインルールだから、UIやDBじゃなく ドメイン(集約)で守るよ🛡️✨


3) ガード節ってなに?🛡️(超ざっくり)

ガード節はこれ👇

  • 「条件がダメなら、最速で止める
  • 深いifネストを作らない
  • “違反” を その場で発見できる

たとえば、支払い済みの注文に pay() しようとしたら… 「え、二重払いじゃん😱」ってなるよね。 だから pay() の最初でこうする👇

if (this.status !== 'Confirmed') {
throw new DomainError('支払いできません(注文が未確定です)');
}

この「最初に止める」がガード節🛡️✨


4) 実装方針:2つのやり方(おすすめは②)🧠✨

① 素直に if を書く(初心者に優しい)😊

  • 分かりやすい
  • でも、増えるとコピペ地獄になりやすい😂

② 遷移表(ルール表)をコードにする(おすすめ)📋✨

  • ルールが1か所にまとまる
  • 状態が増えても壊れにくい
  • テストもしやすい🧪

この章は で行くよ〜!🚀


5) コード:Order集約に「遷移」と「ガード節」を入れる🏯🛡️

ここでは、Order集約ルートだけ載せるね(VOやOrderLineは前章までである前提の形)✍️

5-1) ドメインエラー(例外)を用意する🧯

// domain/errors.ts
export abstract class DomainError extends Error {
override readonly name: string = 'DomainError';
}

export class InvalidOrderOperationError extends DomainError {
constructor(message: string) {
super(message);
}
}

メッセージは「人間が読める」ほうが後で幸せ💞 (ログ向けの詳細は後半章でやるよ〜👀)


5-2) OrderStatus と 遷移表(ルール)を作る🚦📋

// domain/order/Order.ts
import { InvalidOrderOperationError } from '../errors';

export type OrderStatus =
| 'Draft'
| 'Confirmed'
| 'Paid'
| 'Canceled'
| 'Fulfilled';

const allowedTransitions = {
Draft: ['Confirmed', 'Canceled'],
Confirmed: ['Paid', 'Canceled'],
Paid: ['Fulfilled'],
Canceled: [],
Fulfilled: [],
} as const satisfies Record<OrderStatus, readonly OrderStatus[]>;

satisfies を使うと「表が壊れてないか」を型でチェックできて安心だよ🧡 (TypeScriptは5.9系でモジュール周りの設定も整理が進んでるので、最新のルールに寄せやすいよ〜📦✨)(TypeScript)


5-3) ガード用の共通メソッドを作る🛡️✨

export class Order {
private status: OrderStatus;
// private readonly id: OrderId;
// private lineItems: OrderLine[];

private constructor(/* ... */) {
this.status = 'Draft';
}

getStatus(): OrderStatus {
return this.status;
}

// ✅ ガード:条件がダメなら即停止
private ensure(condition: unknown, message: string): asserts condition {
if (!condition) throw new InvalidOrderOperationError(message);
}

// ✅ 遷移表に従っているかを一括チェック
private ensureCanTransitionTo(next: OrderStatus) {
const allowed = allowedTransitions[this.status];
this.ensure(
allowed.includes(next),
`この操作はできません(${this.status}${next} は禁止です)`
);
}

private transitionTo(next: OrderStatus) {
this.ensureCanTransitionTo(next);
this.status = next;
}

// ここからユースケース操作(外部に公開する入り口)🚪👑

ポイントはこれ👇😍

  • statusprivate(外から触れない)🔒
  • transitionTo() が “内部の唯一の変更” になる🧊
  • 遷移が増えても、表と ensureCanTransitionTo() を直せばOK✨

5-4) confirm / pay / cancel / fulfill を実装する☕💳🚫📦

ここで「状態だけ」じゃなく、その操作に必要なルールも一緒にガードするよ🛡️

  confirm() {
// 例:明細ゼロで確定は禁止(ありがち!)
// this.ensure(this.lineItems.length > 0, '注文を確定できません(商品が0件です)');

this.transitionTo('Confirmed');
}

pay() {
// 状態遷移のルールは transitionTo が守ってくれる✨
// 追加で「合計金額が0なら支払いできない」みたいなルールを足してもOK
this.transitionTo('Paid');
}

cancel(reason?: string) {
// 「提供済はキャンセル不可」などは遷移表でブロックされる🛡️
// reasonを持たせたいなら、ここでVO化したり、履歴に残したり(後で拡張OK)
this.transitionTo('Canceled');
}

fulfill() {
this.transitionTo('Fulfilled');
}
}

ここが「DDDっぽいだけ」を卒業する分かれ道🎓✨ アプリ層が if で守るんじゃなく、集約が自分で守るのが大事だよ〜🏯🛡️


6) 使い方イメージ(ミニ例)☕✨

const order = /* Order.create(...) */;

// OK
order.confirm();
order.pay();
order.fulfill();

// NG(例:提供済に pay しようとする)
order.pay(); // ここで例外🧯

この「NGをちゃんと止める」のが今回の勝ち🏆✨


7) よくあるミス集😂⚠️(ここ超大事!)

❌ setStatus を public にする

  • 一発で城が崩壊🏯💥
  • ルールを回避できちゃう

❌ アプリ層で if して “通しちゃう”

  • 画面やAPIが増えるほど、ルールが散らばる🌀
  • 「片方だけチェックしてた😇」が起きる

❌ 遷移表がない(各メソッドにバラバラにif)

  • ルールの見通しが悪くなる
  • 状態が増えた時に事故る🚑

8) AI活用(例外メッセージを “ユーザー向け” に整える🤖💬✨)

この章のAIパートはここが最高に相性いいよ〜!😍

使えるプロンプト例🪄

あなたはUXライターです。
次のドメインエラー文言を、ユーザー向けに短く分かりやすくしてください。
条件:
- 責めない口調
- 次に何をすればいいかが分かる
- 30文字前後

元の文言:
「この操作はできません(Paid → Paid は禁止です)」

さらに開発者向けの情報も欲しいときは👇

同じ内容で、
(1) ユーザー表示用(短い)
(2) ログ用(原因が追える。status/next/operation を含む)
の2種類を提案して。

(エラー設計は後半章で本格的にやるけど、今のうちから “2種類に分ける発想” を入れると強いよ💪✨)


9) 理解チェック(ミニクイズ)📝💡

  1. confirm() の中で ガード節はどこに置くのが気持ちいい?🛡️
  2. 遷移表があると、状態が1個増えたとき何がラク?🚦
  3. 「支払い済の注文は明細変更不可」ってルールは、どこで守る?🏯

答えられたらかなり良い感じ〜!🎉✨


10) 次章の予告🔜🧪

次(第58章)は、今日作った “城” が崩れないかを テストで証明するよ〜!🧪🔒 特に、順番違い(confirmしないでpay等)を大量に叩いて「絶対壊れない」を作るよ🔥

ちなみに最近のテスト界隈だと Vitest 4.0 が出ていて、4.1 beta の動きもあるよ〜📈(高速で気持ちいいやつ!) (Vitest)


必要なら、この章のコードを「ファイル分割(domain/order/…)」した完全版もそのまま出せるよ📁✨