第08章:不変条件(Invariants)って結局なに?🔒🧠
8.1 不変条件って、ひとことで言うと?✨

不変条件(Invariant)は、**「どんな操作の前後でも、絶対に成り立っていてほしいルール」**だよ📏💡 イメージは「校則」みたいなもの👮♀️✨
- 破ったら“ちょっと困る”じゃなくて、世界が壊れるやつ💥
- だから “守れる形”でコードを書くのが目的だよ🧱🔒
8.2 不変条件が必要になる“事故”あるある😱🧨
ミニEC(注文🛒・在庫📦・支払い💳)で、こんな事故が起きがち👇
- 在庫がマイナスになった📉😱
- 合計金額がズレた(明細合計と請求が違う)💸💥
- まだ支払ってないのに出荷された🚚💨
- 二重決済された💳💳😇
- 「キャンセル済み」なのに支払い完了になった🌀
こういうのって、**“データが一瞬でも変な状態になった”**のが原因になりやすいのね🚫🧠 だから「変な状態を作らせない」が超大事💪🔒
8.3 不変条件と“入力チェック”は別モノだよ🧼🆚🔒
混ざりやすいから、ここでスッキリ分けちゃおう😊✨
入力チェック(UIバリデーション)🧼
- 文字数、必須、形式(メールの形📧とか)
- 画面ごとに都合が違うこともある
- すり抜けることがある(API直叩き、バッチ処理、移行データなど)🕳️
不変条件(ドメインの最後の砦)🔒👑
- ドメインとして絶対におかしくならないルール
- どこから操作されても、必ず守られる必要がある
- 破るくらいなら、操作を失敗させる(例外 or Result)🚫
8.4 ミニEC「注文🛒」で不変条件を見つける3ステップ🧩✨
不変条件って、センスじゃなくて手順で出せるよ😊📝
- 「おかしい状態」を日本語で言う🗣️
- 「それが起きると誰が困る?」を言う😵💫
- 「だから絶対に起きない」をルール化する🔒
8.5 注文(Order)の不変条件リスト例📝🔒
ここでは「注文集約」を想定して、よくある不変条件を並べるよ🧺✨ (※この“守る単位”の話は次章でガッツリやるよ🧠🧱)
お金・数量まわり💰📦
- 金額はマイナス禁止🚫💸
- 数量は1以上(0個や-1個はナシ)🚫🔢
- 小計=単価×数量(ズレたら事故)🧮💥
- 合計=明細小計の合計(勝手に改ざん不可)🧾🔒
状態遷移まわり🧾➡️💳➡️🚚
- 支払い(Paid)は、注文確定(Placed)後だけ✅
- 出荷(Shipped)は、支払い完了後だけ✅
- 出荷後はキャンセル不可🚫
- キャンセル済みは支払い不可🚫💳
明細(Items)まわり🛒
- 注文確定後に明細を勝手にいじらない(請求がズレる)🧾💥
- 注文確定時点で明細が空はダメ(“何も買ってない注文”)🌀🚫
8.6 “無効状態を作らない”コードのコツ3つ🧱🔒
ここからが本番だよ〜💪✨ 不変条件は「書いた」だけじゃ守れないので、守れる形にする!
コツ①:プリミティブ直置きしない(VOで囲う)🎁💎
number や string をそのまま使うと、マイナス金額も0個も通っちゃう😇
なので、Money / Quantity みたいな小さな型で守る🛡️
// domain/shared/DomainError.ts
export class DomainError extends Error {
constructor(message: string) {
super(message);
this.name = "DomainError";
}
}
// domain/shared/invariant.ts
export function invariant(condition: unknown, message: string): asserts condition {
if (!condition) throw new DomainError(message);
}
// domain/valueObjects/Money.ts
import { invariant } from "../shared/invariant";
export class Money {
private constructor(private readonly yen: number) {}
static ofYen(yen: number): Money {
invariant(Number.isInteger(yen), "金額は整数(円)でね🪙");
invariant(yen >= 0, "金額はマイナス不可だよ🚫💸");
return new Money(yen);
}
value(): number {
return this.yen;
}
add(other: Money): Money {
return Money.ofYen(this.yen + other.yen);
}
multiply(times: number): Money {
invariant(Number.isInteger(times), "掛け算の回数は整数でね🔢");
invariant(times >= 0, "回数がマイナスはおかしいよ🚫");
return Money.ofYen(this.yen * times);
}
}
// domain/valueObjects/Quantity.ts
import { invariant } from "../shared/invariant";
export class Quantity {
private constructor(private readonly n: number) {}
static of(n: number): Quantity {
invariant(Number.isInteger(n), "数量は整数でね🔢");
invariant(n >= 1, "数量は1以上だよ🚫");
return new Quantity(n);
}
value(): number {
return this.n;
}
}
コツ②:勝手にいじれないようにする(カプセル化📦)
「items配列を外に渡して、外でpushされた」みたいなのが事故の入口😇 なので、外から直接いじれないようにするよ🔒
- フィールドは
private - 外に出すときは
readonly(&必要ならコピー)🧊
コツ③:操作は“状態遷移メソッド”だけにする🚪✨
「支払い」「出荷」みたいな操作は、プロパティ代入じゃなくて、メソッドにする👍 メソッドの中で不変条件を守れるからね🔒💪
// domain/order/Order.ts
import { Money } from "../valueObjects/Money";
import { Quantity } from "../valueObjects/Quantity";
import { invariant } from "../shared/invariant";
export type OrderStatus = "Draft" | "Placed" | "Paid" | "Shipped" | "Cancelled";
export type OrderItem = Readonly<{
productId: string;
unitPrice: Money;
quantity: Quantity;
}>;
export class Order {
private status: OrderStatus = "Draft";
private items: OrderItem[] = [];
private constructor(private readonly id: string) {}
static create(id: string): Order {
invariant(id.trim().length > 0, "注文IDが空だよ🚫");
return new Order(id);
}
getId(): string {
return this.id;
}
getStatus(): OrderStatus {
return this.status;
}
getItems(): ReadonlyArray<OrderItem> {
return this.items; // 外からpushできない(readonly返し)
}
getTotal(): Money {
return this.items.reduce((sum, item) => {
const line = item.unitPrice.multiply(item.quantity.value());
return sum.add(line);
}, Money.ofYen(0));
}
addItem(productId: string, unitPrice: Money, quantity: Quantity): void {
invariant(this.status === "Draft", "確定後に明細はいじれないよ🚫🧾");
invariant(productId.trim().length > 0, "商品IDが空だよ🚫");
this.items.push({ productId, unitPrice, quantity });
this.assertInvariants();
}
place(): void {
invariant(this.status === "Draft", "今の状態だと確定できないよ🚫");
invariant(this.items.length >= 1, "明細が空の注文は確定できないよ🚫🛒");
this.status = "Placed";
this.assertInvariants();
}
markPaid(): void {
invariant(this.status === "Placed", "支払いは注文確定の後だよ💳➡️🧾");
this.status = "Paid";
this.assertInvariants();
}
ship(): void {
invariant(this.status === "Paid", "出荷は支払い完了の後だよ🚚💨");
this.status = "Shipped";
this.assertInvariants();
}
cancel(): void {
invariant(this.status !== "Shipped", "出荷後はキャンセルできないよ🚫🚚");
invariant(this.status !== "Cancelled", "すでにキャンセル済みだよ🌀");
this.status = "Cancelled";
this.assertInvariants();
}
private assertInvariants(): void {
// ✅ 状態に応じて守るルールをまとめてチェック(散らばり防止)
if (this.status === "Placed" || this.status === "Paid" || this.status === "Shipped") {
invariant(this.items.length >= 1, "確定系の状態なのに明細が空はダメ🚫");
}
// ✅ 合計金額がマイナスにならない(Moneyが守ってるけど一応の例)
invariant(this.getTotal().value() >= 0, "合計がマイナスはありえない🚫💸");
}
}
8.7 テストで“不変条件が壊れない”を固定する🧪🔒
不変条件は テストと相性が最高だよ😊✨ 「壊れたら即わかる」=未来の自分が助かる🥹🫶
最近は Vitest 4.0 が出ていて、Vite系と相性よくサクサク回せるよ🧪⚡ (Vitest)
// tests/order.invariants.test.ts
import { describe, it, expect } from "vitest";
import { Order } from "../src/domain/order/Order";
import { Money } from "../src/domain/valueObjects/Money";
import { Quantity } from "../src/domain/valueObjects/Quantity";
describe("Order invariants 🔒", () => {
it("明細が空だと確定できない🛒🚫", () => {
const order = Order.create("o-1");
expect(() => order.place()).toThrowError();
});
it("確定後は明細を追加できない🧾🚫", () => {
const order = Order.create("o-2");
order.addItem("p-1", Money.ofYen(1200), Quantity.of(1));
order.place();
expect(() => order.addItem("p-2", Money.ofYen(500), Quantity.of(1))).toThrowError();
});
it("支払い前に出荷できない🚚🚫", () => {
const order = Order.create("o-3");
order.addItem("p-1", Money.ofYen(1000), Quantity.of(2));
order.place();
expect(() => order.ship()).toThrowError();
});
it("合計が明細の合計になる💰✅", () => {
const order = Order.create("o-4");
order.addItem("p-1", Money.ofYen(1000), Quantity.of(2)); // 2000
order.addItem("p-2", Money.ofYen(500), Quantity.of(1)); // 500
expect(order.getTotal().value()).toBe(2500);
});
});
8.8 AI活用🤖✨:不変条件を“漏れなく・具体的に”するプロンプト例
AIは「候補出し」と「事故の具体化」が超得意だよ🧠⚡ (最後に決めるのは人間ね😉🫶)
候補を増やす📝
- 「ミニECの注文ドメインで、不変条件候補を15個。破れた時の事故例も1行ずつつけて」
ルールの粒度を整える📏
- 「この不変条件は“ドメイン不変条件”?それとも“入力チェック”?理由も」
コードレビュー風に点検👀
- 「このOrderクラスの不変条件が抜けてるところを指摘して、
assertInvariants()に追加案を出して」
8.9 ミニコラム:2026年のTypeScript/Nodeまわりの“いま”📰✨
- TypeScript は公式サイト上で “latest は 5.9” と案内されているよ🧷 (typescriptlang.org)
- Node.js は v24 が Active LTS、v25 が Current として更新が続いてるよ🟩🆙 (nodejs.org)
- TypeScript のツール群をネイティブ移植する動き(TypeScript 7 Native Preview)も進んでいて、大規模プロジェクトの体験が良くなりそうって話題だよ🚀 (Microsoft Developer)
(この章の“不変条件”自体は、ツールのバージョンが変わっても一生モノの考え方だよ🔒✨)
8.10 まとめ📦✨
-
不変条件は **「絶対に壊れちゃダメなルール」**🔒
-
事故は “一瞬でも変な状態になった” ところから起きやすい😱
-
守り方の基本はこれ👇
- VOで型に閉じ込める💎
- 勝手に触れないようにカプセル化📦
- 状態遷移メソッドでチェック🚪
- テストで固定🧪✅
次章は「じゃあ、その不変条件を守る“まとまり”って何?」= 集約(Aggregate) に入っていくよ🧺✨