第18章 ドメイン入門③:ミニ不変条件(Invariants)🧷✨
![hex_ts_study_018[(./picture/hex_ts_study_018_value_objects.png)
この章はね、「どんな入口(CLI/HTTP/将来のGUI)が増えても、アプリのルールが絶対に壊れない」ようにする回だよ〜😊💖 ヘキサゴナルでいう「中心を守る🛡️」のいちばん地味で、いちばん強い武器が“不変条件”なのです✨
0. まず今日のゴール🎯✨
今日できるようになること👇😊
-
不変条件(Invariants)って何かを、自分の言葉で説明できる📣✨
-
Todoドメインの不変条件を2つ、中心(domain)で守れる🛡️
- ✅ タイトル空はNG
- ✅ 完了の二重適用はNG(もう完了してるのにまた完了…はダメ🙅♀️)
-
入口が増えてもルールが崩れない理由がわかる🔌🧩
1. 不変条件(Invariants)ってなに?🧷
超かんたんに言うと👇
「この世界(ドメイン)では、絶対にこうでなきゃダメ!」 っていう“ルールの骨格”💀✨
たとえば Todo なら…
- タイトルが空の Todo は存在してほしくない😵💫
- 「完了」って1回だけでいいのに、2回目以降も通ったら変だよね?😱
これが 不変条件だよ🧷✨
2. なんで“不変条件”をドメインに置くの?🏰🛡️
ここがヘキサゴナルの気持ちよさポイント😍✨
✅ 入口(Adapter)は増える前提
最初はCLIだけでも、あとからHTTP APIが増えたり、管理画面が増えたりするよね🌐📱
もし不変条件を入口側に散らすと…
- CLIではチェックしてたのに、HTTP側でチェック忘れて事故💥
- 入口が増えるたび、同じチェックをコピペ地獄😵💫📄📄📄
だから👇
✅ “中心(domain)”で守れば、全部の入口が安全になる🛡️✨
入口がどれだけ増えても、中心が最後の砦になってくれるの💪🔥
3. “入力チェック”と“不変条件”のちがい🧠✨
ここ、混ざりがちだから整理するね😊📌
入力チェック(入口でやりがち)🚪
- 文字列か?数値か?
- 必須項目が来てるか?
- JSONの形が正しいか?
👉 これは 入口(Inbound Adapter) が得意✨
不変条件(ドメインが守る)🏰
- 正しいTodoとして存在できるか?
- 状態遷移が変じゃないか?(例:完了の二重適用)
👉 これは domain の責任🛡️✨
※入口で軽く弾いてもOKだけど、最終的にdomainでも必ず守るのが安心だよ😊
4. 実装しよう:Todoの“不変条件2つ”📝✨
ここから、コードで「中心を守る🛡️」体験いくよ〜!💻🎉
(ファイル場所は、例として src/domain/todo/ に置く想定📁)
4.1 ドメイン側の最小コード(Todo + 不変条件)🧩🧷
ポイントはこれ👇😊
- 生成時に「タイトル空NG」を守る
- 完了処理で「二重完了NG」を守る
- 外から勝手に壊されないように、
Readonlyと “更新は関数で返す” を使う✨
// src/domain/todo/Todo.ts
export type TodoId = string;
export type Todo = Readonly<{
id: TodoId;
title: string;
completed: boolean;
}>;
// いったん最小のドメインエラー(後の章で整備するよ😊)
export class DomainError extends Error {
constructor(message: string) {
super(message);
this.name = "DomainError";
}
}
function normalizeTitle(raw: string): string {
// 「空っぽ」「空白だけ」をまとめて弾きたいので trim() するよ✂️
return raw.trim();
}
/**
* ✅ 不変条件:タイトル空NG を守りながら Todo を作る
*/
export function createTodo(params: { id: TodoId; title: string }): Todo {
const title = normalizeTitle(params.title);
if (title.length === 0) {
throw new DomainError("Todoのタイトルは空にできません🥺");
}
return {
id: params.id,
title,
completed: false,
} as const;
}
/**
* ✅ 不変条件:完了の二重適用NG を守りながら完了にする
*/
export function completeTodo(todo: Todo): Todo {
if (todo.completed) {
throw new DomainError("このTodoは既に完了しています(二重完了NG)🙅♀️");
}
return {
...todo,
completed: true,
} as const;
}
5. いまのコード、どこが“ヘキサゴナル的に良い”の?🔌🧩✨
✅ 入口が何であれ、domainが最後に守る🛡️
CLIでもHTTPでも、最終的に createTodo / completeTodo を通る限り、ルールは破れない💖
✅ 状態遷移の芽がある🌱
completed: false → true の変化は、のちの章で「状態機械っぽい考え方🚦」にも繋げられるよ😊✨
6. すぐ試せる!ミニテスト(Vitest)🧪✨
不変条件は テストで守ると超つよい💪💖 Vitestは今も活発に更新されてて、移行ガイドも整備されてるよ📘✨ (Vitest)
// src/domain/todo/Todo.test.ts
import { describe, it, expect } from "vitest";
import { createTodo, completeTodo, DomainError } from "./Todo";
describe("Todo invariants 🧷", () => {
it("タイトル空はNG", () => {
expect(() => createTodo({ id: "1", title: " " })).toThrow(DomainError);
});
it("完了の二重適用はNG", () => {
const todo = createTodo({ id: "1", title: "Buy milk" });
const done = completeTodo(todo);
expect(done.completed).toBe(true);
expect(() => completeTodo(done)).toThrow(DomainError);
});
it("タイトルは前後の空白を落として保存される", () => {
const todo = createTodo({ id: "1", title: " Hello " });
expect(todo.title).toBe("Hello");
});
});
7. よくある事故パターン😱💥(先に潰す!)
❌ 入口にしかチェックがない
HTTPでは弾けてたのに、バッチ処理から直に保存して壊れる…みたいな事故が起きる💥
❌ “どこでもif”で散らかる
あちこちに if (title === "") が出てきたら黄色信号🚥😵💫
→ domainに寄せていこう✨
❌ 「とりあえずany」で通す
不変条件があっても、型が雑だと穴が増える🕳️ → “中心ほど型を丁寧に” が安定するよ😊
8. AI活用コーナー🤖💖(この章に効く使い方)
✅ 不変条件の洗い出しを手伝ってもらう
コピペで使えるよ👇📝✨
- 「Todoドメインの不変条件を10個提案して。初心者向けに理由も」
- 「この
createTodo/completeTodoに抜けてる不変条件がないかレビューして」 - 「不変条件ごとに、Vitestのテストケースを提案して」
⚠️ でもここは自分で決める!
何が“ルール”かは、アプリの意図そのものだからね😊🛡️
9. 自主ミニ課題📝🎀
できそうなのを1個だけでOKだよ〜😊✨
課題A:タイトル最大50文字ルールを追加✂️
title.length > 50をドメインで弾く- テストも追加🧪
課題B:「未完了に戻す」を作ってみる🔁
- ただしルールを自分で決めてね(戻せる?戻せない?)😊
10. まとめ🎁✨(今日の合言葉)
- 不変条件は 「絶対に守りたいルール」 🧷
- 入口が増えても壊れないように、domainが最後に守る 🏰🛡️
- 「タイトル空NG」「二重完了NG」みたいな 小さいルールから始めるのがコツ🌱
2026年1月の“いま”メモ🗓️✨(最新版チェック)
- TypeScript の最新リリースノート(5.9)が公式ドキュメントで更新されてるよ🧠✨ (typescriptlang.org)
- Node.js は v24 が Active LTS として案内されてるよ🔒✨ (Node.js)
(バージョンは今後も動くから、迷ったら公式のリリースページを見るのが安全だよ😊)
次の章(第19章)では、このドメインを使って ユースケース(Add / Complete / List) を作って「手順」と「判断」を中心に置くよ〜🎮➡️🧠✨