メインコンテンツまでスキップ

第9章 関数DI③:PureロジックとI/Oを切り分ける🍱

今日のゴール🎯

  • 「I/O(外の世界とのやりとり)」と「Pure(純粋ロジック)」の違いがわかる😊
  • “中心はPure、外側でI/O” の2層でコードを書ける🍩
  • テストがラクになって「DIってこれか〜!」ってなる🧪💕

1) I/Oってなに?(外の世界)🌍📦

I/Oは、アプリの外側とつながる行為だよ〜!

たとえば👇

  • 時間:Date.now() / new Date()
  • 乱数:Math.random() 🎲
  • 通信:fetch() / API呼び出し 🌐
  • 保存:localStorage / ファイル / DB 💾
  • 環境:process.env ⚙️
  • ログ:console.log() 🗣️

I/Oは便利だけど、テストを不安定にしがち(時間や通信が毎回変わるから)😵‍💫


2) Pureロジックってなに?(内側の世界)🫧

Pure(純粋)な関数は、ざっくり言うと👇

  • 同じ入力 → 同じ出力(毎回同じ✨)
  • 外部に触らない(通信しない・保存しない・時間見ない)
  • 副作用なし(ログも本当は副作用だよ!)

だからPureは、テストが超簡単💖 「入力これね → 出力これね」を確認するだけでOK🧪


2) 目指す形:「ドーナツ構造」🍩✨

イメージはこれ👇

  • 🍩 中心(Functional Core):Pureロジック(判断・計算・ルール)
  • 🍩 外側(Imperative Shell):I/O(取得・保存・通信・ログ)

そして第9章のポイントはこれ💡

I/Oは外側に追い出して、中心をPureにする✂️✨


4) まずは「分離できてない例」😣

学習記録を保存する関数、ありがちなやつ👇(※わざとゴチャゴチャ)

// 😣 I/Oが全部まざってる例
export async function recordStudy(subject: string, minutes: number) {
const date = new Date().toISOString().slice(0, 10); // I/O(時間) ⏰
const raw = localStorage.getItem("study") ?? '{"entries":[]}'; // I/O(保存) 💾
const state = JSON.parse(raw);

state.entries.push({ date, subject, minutes }); // ここはロジックっぽい🍱

localStorage.setItem("study", JSON.stringify(state)); // I/O(保存) 💾
console.log("saved!"); // I/O(ログ) 🗣️

return state;
}

このままだとテストがつらい…

  • new Date() が毎回変わる⏰
  • localStorage をテストで用意しないといけない💾
  • ログの検証も混ざる🗣️

5) 切り分けの手順(3ステップ)✂️✨

✅ ステップ1:I/O行に「I/O」って印をつける🟥

まずは目で見える化👀✨

✅ ステップ2:Pureだけを別関数に抜く🍱

「状態(state)に entry を追加する」みたいな中心ルールだけ残す!

✅ ステップ3:I/Oは外側に置いて deps で注入👜

外側が clockrepo を持って、中心に材料だけ渡す🥕🍳


6) 完成形サンプル:学習記録(CoreとShell)📚💕

🍱 (A) Pureロジック:core(触っていいのはデータだけ!)

// core/studyCore.ts
export type StudyEntry = {
date: string; // "2026-01-16" みたいなISO日付
subject: string;
minutes: number;
};

export type StudyState = {
entries: StudyEntry[];
};

// ✅ Pure:state と entry だけで決める(I/O禁止)
export function addEntry(state: StudyState, entry: StudyEntry): StudyState {
// ルール例:0分以下は追加しない(お好みでthrowでもOK)
if (!entry.subject.trim()) return state;
if (entry.minutes <= 0) return state;

return {
...state,
entries: [...state.entries, entry],
};
}

🌍 (B) 外側:shell(I/O担当)+ 関数DI(deps→input)

// app/recordStudy.ts
import { addEntry, type StudyState } from "../core/studyCore";

type Deps = {
clock: { todayISO(): string }; // ⏰ 今日の日付を返す
repo: {
load(): Promise<StudyState>; // 💾 読む
save(state: StudyState): Promise<void>; // 💾 書く
};
logger: { info(message: string): void; error(message: string, err?: unknown): void }; // 🗣️
};

// ✅ 関数DI:depsを先にもらって、あとからinput
export const makeRecordStudy =
(deps: Deps) =>
async (input: { subject: string; minutes: number }) => {
try {
// I/Oはここ(外側)に集める✨
const date = deps.clock.todayISO(); // ⏰
const state = await deps.repo.load(); // 💾

// 🍱 中心はPureに任せる
const next = addEntry(state, { date, ...input });

await deps.repo.save(next); // 💾
deps.logger.info(`saved: ${input.subject} ${input.minutes}min`); // 🗣️

return next;
} catch (err) {
deps.logger.error("recordStudy failed", err);
throw err; // ここは方針しだい(Result型で返すでもOK)
}
};

💾 (C) 例:localStorage repo(ブラウザ用のI/Oアダプタ)

// infra/localStorageStudyRepo.ts
import type { StudyState } from "../core/studyCore";

const KEY = "study";

export function createLocalStorageStudyRepo(): {
load(): Promise<StudyState>;
save(state: StudyState): Promise<void>;
} {
return {
async load() {
const raw = localStorage.getItem(KEY) ?? '{"entries":[]}';
return JSON.parse(raw) as StudyState;
},
async save(state) {
localStorage.setItem(KEY, JSON.stringify(state));
},
};
}

7) テストがどうラクになるか🧪💖

✅ Pure(core)は「そのままテスト」できる!

// core/studyCore.test.ts
import { test, expect } from "vitest";
import { addEntry } from "./studyCore";

test("addEntry: 正常に1件追加される", () => {
const state = { entries: [] as any[] };
const next = addEntry(state, { date: "2026-01-16", subject: "DI", minutes: 30 });

expect(next.entries).toHaveLength(1);
expect(next.entries[0].subject).toBe("DI");
});

test("addEntry: 0分は追加されない", () => {
const state = { entries: [] as any[] };
const next = addEntry(state, { date: "2026-01-16", subject: "DI", minutes: 0 });

expect(next.entries).toHaveLength(0);
});

Vitestは「Vite系の次世代テストフレームワーク」として案内されていて、ガイドも読みやすいよ🧪✨ (Vitest)

✅ Shell(I/Oあり)は Fake / Spy で安定化

// app/recordStudy.test.ts
import { test, expect } from "vitest";
import { makeRecordStudy } from "./recordStudy";

test("makeRecordStudy: 保存される(I/OはFake)", async () => {
const saved: any[] = [];

const deps = {
clock: { todayISO: () => "2026-01-16" },
repo: {
async load() { return { entries: [] }; },
async save(state: any) { saved.push(state); },
},
logger: { info: (_: string) => {}, error: (_: string) => {} },
};

const record = makeRecordStudy(deps);
const next = await record({ subject: "TypeScript", minutes: 25 });

expect(next.entries).toHaveLength(1);
expect(saved).toHaveLength(1);
});

8) よくあるミス集(ここ気をつけて〜!)⚠️😵‍💫

  • ❌ Pure関数の中で Date.now() しちゃう(時間I/O混入)⏰
  • ❌ Pure関数の中で console.log() しちゃう(ログも副作用)🗣️
  • deps を巨大にしすぎる(何でも屋バッグ👜が重くなる)🧱
  • ❌ Shellが太って、結局ロジックが戻ってくる(中心に寄せる意識!)🍩

9) ミニ課題📝✨(この章の練習!)

課題1️⃣:I/Oハイライトゲーム🟥

手元のコードで、I/O行にコメントで // I/O を付けてみてね👀

課題2️⃣:Pure抽出✂️🍱

I/Oを消した「判断だけの関数」を1つ作って、core/ に移動!

課題3️⃣:テスト1本🧪

  • core関数は「入力→出力」テスト
  • shellはFake depsで「保存された?」だけテスト

10) AI活用(Copilot/Codexに頼むコツ)🤖💡

使いやすい指示テンプレ👇(コピペしてOK✨)

  • 「この関数からI/O行を見つけて、Pure関数(core)とShell(app)に分割して」
  • 「Pure関数のユニットテストをVitestで3本作って。境界ケースも入れて」
  • 「depsを最小化したい。今のdepsから本当に必要なものだけに絞って提案して」

11) ちょい最新メモ(2026-01-16時点)🗓️✨

  • TypeScript 5.9 のリリースノートでは、Node向けに挙動が安定した --module node20 が紹介されていて、Node系プロジェクトの“環境依存のゆれ”を減らしやすい流れだよ🔧✨ (TypeScript)
  • Node.js は 2026-01-13 に 24.13.0 (LTS) のセキュリティリリースが出てるので、実運用は「更新大事!」って感じ🛡️ (Node.js)
  • Bun は 2026-01-13 に v1.3.6 が出てるよ⚡(Windowsの導入手順も載ってる) (bun.com)

まとめ🎀🏁

第9章の合言葉はこれっ💖

  • 中心はPure(判断・計算)🍱
  • 外側でI/O(取得・保存・通信)🌍
  • つなぎ目は関数DI(deps→input)👜✨

次の章で「クラスDI(コンストラクタ注入)」に行く前に、この🍩感覚が入ってると超強いよ〜!💪🥰