第45章:最終チェック:差し替え実演(外側を変えても中心が無傷)🎉🔁
この章は「クリーンアーキって結局なにが嬉しいの?」を、動くデモで完全に腹落ちさせる回だよ〜!🥳✨ やることは超シンプル👇
- ✅ InMemory Repository ↔ ✅ SQLite Repository を切り替える
- ✅ それでも Entities / UseCases は1行も変えない
- ✅ しかも テストが同じまま通る(ここ超重要!🧪🔥)
0) 2026/01/23時点の“土台”だけ確認 🧱✨
- TypeScript の最新安定版は 5.9.3(npm の latest が 5.9.3)だよ 📌 (npmjs.com)
- Node.js は v24 が Active LTS、v25 が Current(最新系)って位置づけだよ 📌 (Node.js)
- SQLite は Node 標準の
node:sqliteが使えて、DatabaseSync/statement.get()/statement.all()などが用意されてるよ 🗃️ (Node.js) - テストは Vitest 4 が現行メジャー(4.0 アナウンス済み)🧪 (vitest.dev)
1) 今日のゴール(これができたら卒業🎓)🎯✨
ゴールA:差し替えで“中心が無傷”を証明する 🛡️
- ✅ 差し替えるのは Composition Root(組み立て)だけ
- ✅ UseCase / Entity / Port は一切変更しない
ゴールB:テストで“証拠”を残す 🧪📸
-
✅ 同じ UseCase テストを
- InMemory Repo でも
- SQLite Repo でも どっちでも通す✨
2) 差し替えが成立するための“絶対条件”🚧
差し替えで詰まる原因って、だいたいこれ👇
✅ Port(Repository interface)の契約が「DB都合」になってる
たとえばこんなのは危険⚠️
- ❌
findById(id: number)(DBの型に寄ってる) - ❌
listRawRows()(SQLの行を内側へ持ち込む)
理想はこう👇
- ✅ “内側の言葉”(Task / TaskId / completed など)で話す
- ✅ “保存・取得”という能力だけが見える
3) 差し替えポイントはここだけ!🎯(Composition Root)
この章の主役は **「組み立て」**だよ🏗️✨ Repository を どれにするかをここで決める!
イメージ👇
-
CreateTaskInteractorはTaskRepositoryしか知らない -
だから
TaskRepositoryの実体をInMemoryTaskRepositoryにしてもSqliteTaskRepositoryにしても UseCase は何も変わらない🎉
4) “切り替えスイッチ”を1つ作る 🎛️✨
環境変数で切り替えるのが分かりやすいよ〜!(デモ向き💡)
PowerShell で切り替え(例)🪟
- InMemory で起動:
$env:TASK_STORE="memory" - SQLite で起動:
$env:TASK_STORE="sqlite"
5) Composition Root の最小例(ここだけ変えればOK)🏗️💉
// compositionRoot.ts
import { CreateTaskInteractor } from "../usecases/createTask/CreateTaskInteractor";
import { CompleteTaskInteractor } from "../usecases/completeTask/CompleteTaskInteractor";
import { ListTasksInteractor } from "../usecases/listTasks/ListTasksInteractor";
import { InMemoryTaskRepository } from "../adapters/outbound/InMemoryTaskRepository";
import { SqliteTaskRepository } from "../adapters/outbound/SqliteTaskRepository";
type StoreKind = "memory" | "sqlite";
function resolveStoreKind(): StoreKind {
const v = (process.env.TASK_STORE ?? "memory").toLowerCase();
return v === "sqlite" ? "sqlite" : "memory";
}
export function buildApp() {
const kind = resolveStoreKind();
const taskRepo =
kind === "sqlite"
? new SqliteTaskRepository({ dbPath: "./data/tasks.db" })
: new InMemoryTaskRepository();
// UseCase は Port しか知らない(ここが最高にクリーン✨)
const createTask = new CreateTaskInteractor(taskRepo);
const completeTask = new CompleteTaskInteractor(taskRepo);
const listTasks = new ListTasksInteractor(taskRepo);
return { createTask, completeTask, listTasks, kind };
}
✅ 重要ポイントはこれだけ👇
new InMemoryTaskRepository()とnew SqliteTaskRepository()を切り替えてるのは ここだけ- UseCase 側は
taskRepoが何者か知らない😌✨
6) SQLite Adapter(node:sqlite を使う最小の雰囲気)🗃️✨
「もう作ってある」前提でもいいんだけど、差し替えの説得力が上がるから “雰囲気だけ”載せるね☺️
(Node 標準 node:sqlite の DatabaseSync / prepare() / run() / get() / all() を使う) (Node.js)
// SqliteTaskRepository.ts(雰囲気の最小例)
import { DatabaseSync } from "node:sqlite";
import { TaskRepository } from "../../ports/TaskRepository";
import { Task } from "../../entities/Task";
export class SqliteTaskRepository implements TaskRepository {
private db: DatabaseSync;
constructor(private opts: { dbPath: string }) {
this.db = new DatabaseSync(opts.dbPath);
this.db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
completed INTEGER NOT NULL
) STRICT;
`);
}
async save(task: Task): Promise<void> {
const st = this.db.prepare(`
INSERT INTO tasks (id, title, completed)
VALUES (?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
title = excluded.title,
completed = excluded.completed;
`);
st.run(task.id.value, task.title.value, task.completed ? 1 : 0);
}
async findById(id: string): Promise<Task | null> {
const st = this.db.prepare(`SELECT id, title, completed FROM tasks WHERE id = ?;`);
const row = st.get(id) as undefined | { id: string; title: string; completed: number };
if (!row) return null;
return Task.rebuild({ id: row.id, title: row.title, completed: row.completed === 1 });
}
async listAll(): Promise<Task[]> {
const st = this.db.prepare(`SELECT id, title, completed FROM tasks ORDER BY rowid DESC;`);
const rows = st.all() as Array<{ id: string; title: string; completed: number }>;
return rows.map(r => Task.rebuild({ id: r.id, title: r.title, completed: r.completed === 1 }));
}
}
ここでの“勝ち筋”は👇
- ✅ SQL の行(row)を Mapper 的にここで吸収
- ✅ 内側へ返すのは Task(内側のモデル)だけ
7) いよいよ本番:差し替えデモ 🎬🎉

デモ①:InMemory で動かす(起動し直すと消える)🧺💨
$env:TASK_STORE="memory"- 起動
- Task を Create / List
- 再起動
- 👉 消える(メモリだから!)
デモ②:SQLite で動かす(起動し直しても残る)🗃️✨
$env:TASK_STORE="sqlite"- 起動
- Task を Create / List
- 再起動
- 👉 残る(ファイルDBだから!)
✅ なのに… UseCase も Entity も 1行も変えてないはず!🎉 これが “中心が無傷” の証明だよ〜!🛡️✨
8) 最強の証拠:同じ UseCase テストを両方で回す 🧪🔥
ここが一番気持ちいいところ!!🥹✨ Vitest は v4 系が現行メジャーだよ🧪 (vitest.dev)
ポイントは describe.each で同じテストを使い回すこと👇
// createTask.spec.ts
import { describe, it, expect } from "vitest";
import { CreateTaskInteractor } from "../usecases/createTask/CreateTaskInteractor";
import { InMemoryTaskRepository } from "../adapters/outbound/InMemoryTaskRepository";
import { SqliteTaskRepository } from "../adapters/outbound/SqliteTaskRepository";
type Kind = "memory" | "sqlite";
function makeRepo(kind: Kind) {
if (kind === "sqlite") {
// テスト用に別ファイルへ(毎回消してもOK)
return new SqliteTaskRepository({ dbPath: "./data/test.tasks.db" });
}
return new InMemoryTaskRepository();
}
describe.each<Kind>(["memory", "sqlite"])("CreateTask (%s)", (kind) => {
it("creates a task", async () => {
const repo = makeRepo(kind);
const uc = new CreateTaskInteractor(repo);
const res = await uc.execute({ title: "Buy milk" });
expect(res.ok).toBe(true);
const list = await repo.listAll();
expect(list.length).toBe(1);
expect(list[0].title.value).toBe("Buy milk");
});
});
✅ これが通ったら、あなたはもう勝ち確🎊
- Repository を変えても
- UseCase のテストが
- 同じまま通る
9) うまく差し替えできない時の“漏れチェック”🕵️♀️⚠️
差し替えで失敗するときの典型👇
❌ UseCase が DB の都合を知ってしまってる
- 「SQLite は rowid が〜」みたいな発想が UseCase に出たらアウト😵
❌ Port が “巨大化” or “SQL便利メソッド化”
findByTitleLike()とか増やしすぎると、次のDBで死ぬ🪦
✅ 正しい修正方針
- “必要な能力”として Port を設計し直す
- 変換・最適化は Adapter 側に寄せる
10) 今日の提出物(これができたら完了✅)📦✨
- ✅
TASK_STORE=memory/sqliteの切り替えで動作が変わる - ✅ 切り替えで Entities / UseCases が一切変更なし
- ✅ UseCase テストが 両方のRepoで通る🧪
11) AI相棒に投げると爆速になるプロンプト集 🤖⚡
- 🤖「
TaskRepositoryのメソッド、差し替えに弱い匂いがあるかレビューして。改善案も」 - 🤖「
describe.eachで InMemory/SQLite を同じテストで回す構成にして」 - 🤖「UseCase がDB都合を知ってしまってる箇所がないか、依存・命名・戻り値の観点で監査して」
- 🤖「SQLite Adapter の例外を、外側の責務として扱う方針(握りつぶさない)を整理して」
ラスト🎉:この章で“体に入る”こと
クリーンアーキは「図がキレイ」じゃなくて、 “外側の変更”に対して、中心を守れるのが本当の価値だよ🛡️✨
差し替えデモ+両対応テストが通ったら、あなたはもう卒業〜!🎓🌸