第14章:永続化②:Repositoryで差し替え可能にする🔁🧲
この章のゴール🎯✨
- 「保存する処理(LocalStorageなど)」をアプリの本体ロジックから引きはがす✂️
interface(契約)を挟んで、保存先を あとから差し替え できるようにする🔁- 次の章(DI/テスト)に超つながる形に整える🧪🔌
1) まず“つらみ”を確認しよ🥺💦
第13章で localStorage 保存ができた!えらい!🎉
でも…もし Service の中にこんなのが入ってたらどうなる?👇
- Serviceが
localStorage.getItem()を直接呼ぶ - JSONの
parse/stringifyもServiceがやる - 保存キー文字列もServiceが知ってる
こうなると将来、保存先を👇に変えたい時に地獄😇💥
- IndexedDBにしたい📦(大量データ向き)
- サーバーAPIにしたい🌐
- テスト用にメモリ保存にしたい🧸
そこで登場!Repository✨ 「保存の詳細は外側に追い出して、アプリ本体は“保存できる”という事実だけを知る」っていう考え方だよ🧠💡 (Repositoryは永続化の関心事をドメイン(本体)から外へ出すためのパターンとして説明されることが多いよ)(Microsoft Learn)
2) 今回の完成形の“依存の向き”🚦➡️

イメージはこれ👇(矢印=「知ってる/呼べる」)
- Controller → Service → ITodoRepository(interface)
- LocalStorageTodoRepository → ITodoRepository(interface)
- (Serviceは LocalStorage を知らない🙅♀️)
「本体(Service)が、外部の都合(LocalStorage)を知らない」状態が作れたら勝ち🏆✨
3) 重要ポイント:LocalStorageは“文字列の箱”📦🧵
LocalStorageは キー/値のペアで、キーは文字列で扱われるよ🗝️🧾(MDN Web Docs)
あと、localStorage は基本的に 同期的(処理がその場で走る)だから、デカいデータを頻繁に出し入れすると重くなりがち⚠️(MDN Web Docs)
だからこそ、
- JSON変換
- 例外処理(壊れたデータ対策)
- 保存キー管理
このへんは Repository側に隔離しようね😉✨
4) 実装していこう〜!🧑💻✨(CampusTodo)
4-1. フォルダ構成(例)📁
src/model/… TodoItemなど(アプリの中心の型)src/repository/… interfaceと実装(保存の世界)src/service/… 操作の本体(追加/完了/編集)src/main.ts… 組み立て(どのRepositoryを使うか決める場所)
5) Step1:Repositoryの“契約”を作る📜✨

まずは最小でOK! このアプリは「一覧をまとめて保存/読み込み」できれば十分なので、こうしちゃう👇
// src/repository/ITodoRepository.ts
import type { TodoItem } from "../model/TodoItem";
export interface ITodoRepository {
loadAll(): TodoItem[];
saveAll(todos: TodoItem[]): void;
}
ポイント💡
- Serviceは
ITodoRepositoryだけ知っていればいい🫶- 保存先がLocalStorageでも、IndexedDBでも、サーバーでも関係なし🎉
6) Step2:LocalStorage実装を“外側”に作る💾🧱
6-1. Dateがあるなら注意!⏰⚠️
Date は JSON.stringify() で文字列になっちゃうから、読み込み時に new Date() で戻す必要があるよ🌀
(dueDate/createdAtを使ってる想定で例を書くね)
// src/repository/LocalStorageTodoRepository.ts
import type { ITodoRepository } from "./ITodoRepository";
import type { TodoItem } from "../model/TodoItem";
type StoredTodoItem = Omit<TodoItem, "dueDate" | "createdAt"> & {
dueDate?: string; // ISO文字列
createdAt: string; // ISO文字列
};
const STORAGE_KEY = "campusTodo.todos.v1";
function toStored(todo: TodoItem): StoredTodoItem {
return {
...todo,
dueDate: todo.dueDate ? todo.dueDate.toISOString() : undefined,
createdAt: todo.createdAt.toISOString(),
};
}
function fromStored(todo: StoredTodoItem): TodoItem {
return {
...todo,
dueDate: todo.dueDate ? new Date(todo.dueDate) : undefined,
createdAt: new Date(todo.createdAt),
};
}
export class LocalStorageTodoRepository implements ITodoRepository {
loadAll(): TodoItem[] {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
try {
const stored = JSON.parse(raw) as StoredTodoItem[];
if (!Array.isArray(stored)) return [];
return stored.map(fromStored);
} catch {
// 壊れたJSONが入っててもアプリが落ちないように保険🧯
return [];
}
}
saveAll(todos: TodoItem[]): void {
const stored = todos.map(toStored);
const json = JSON.stringify(stored);
localStorage.setItem(STORAGE_KEY, json);
}
}
ここが最高ポイント🥳
- JSONの事情、保存キー、例外処理、Date変換…全部Repositoryの中!
- Serviceは「配列を渡したら保存される」だけ知ってればOK💞
7) Step3:Serviceを“Repository経由”に統一する🍔➡️🥗✨
Serviceから localStorage を完全に消す!🔥
// src/service/TodoService.ts
import type { ITodoRepository } from "../repository/ITodoRepository";
import type { TodoItem } from "../model/TodoItem";
export class TodoService {
private todos: TodoItem[] = [];
constructor(private readonly repo: ITodoRepository) {}
load(): ReadonlyArray<TodoItem> {
this.todos = this.repo.loadAll();
return this.todos;
}
getSnapshot(): ReadonlyArray<TodoItem> {
return this.todos;
}
add(todo: TodoItem): void {
this.todos = [todo, ...this.todos];
this.repo.saveAll(this.todos);
}
toggleDone(id: string): void {
this.todos = this.todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
this.repo.saveAll(this.todos);
}
// もし必要なら delete / edit も同じノリでOK🧡
}
8) Step4:差し替えポイントは“組み立て側”だけ🧩✨
main.ts(またはComposition Root)で「どのRepositoryを使うか」を決めるよ🎛️
// src/main.ts
import { LocalStorageTodoRepository } from "./repository/LocalStorageTodoRepository";
import { TodoService } from "./service/TodoService";
// (ViewやControllerの生成は既存のままでOK想定)
const repo = new LocalStorageTodoRepository();
const service = new TodoService(repo);
// controller.init() の中で service.load() → view.render() みたいに繋ぐと綺麗🎀
これで、保存先を変える時は ここだけいじればOK🎉🎉🎉
9) ミニ演習:メモリRepositoryに差し替えてみよ🧸✨
「保存しないけど動作確認したい」って時に便利!
// src/repository/InMemoryTodoRepository.ts
import type { ITodoRepository } from "./ITodoRepository";
import type { TodoItem } from "../model/TodoItem";
export class InMemoryTodoRepository implements ITodoRepository {
private data: TodoItem[] = [];
loadAll(): TodoItem[] {
return [...this.data];
}
saveAll(todos: TodoItem[]): void {
this.data = [...todos];
}
}
main.ts の差し替えは1行!🥳
// const repo = new LocalStorageTodoRepository();
const repo = new InMemoryTodoRepository();
これ、次の章の「FakeRepositoryでテスト」がほぼ同じ形になるよ🧪✨
10) よくあるミス集(先に潰す!)🚧😇
- ServiceがlocalStorageを呼んでる → Repository導入の意味が半減🥲(Serviceはinterfaceだけ見よう)
- JSON.parseで落ちる → try/catch必須🧯(壊れたデータは現実に起きる)
- Dateが文字列のままでバグる
→
toStored/fromStoredみたいに変換を置くと安全💖 - 保存キーがあちこちに散らばる
→ Repository内で
STORAGE_KEYを1つにまとめる🧷
11) AI活用プロンプト例🤖💡(“最小に削る”のがコツ✂️)
interface案を出してもらう🧾
- 「Todoの保存を差し替え可能にしたい。最小の
ITodoRepositoryを提案して。メソッドは2〜4個まで。」
実装のレビューをしてもらう🧐
- 「このServiceがLocalStorageに依存してないか確認して。依存してたら、どこをRepositoryに移すべきか指摘して。」
事故対策チェック🧯
- 「JSONが壊れてても落ちない実装になってる?危ない箇所を列挙して直して。」
12) 章末チェック✅✨(できたら勝ち!)
-
ITodoRepositoryを作った - Serviceから
localStorageが消えた🙅♀️ - LocalStorage実装はRepositoryに閉じ込めた📦
-
main.tsでRepositoryを差し替えできた🔁🧲 - 変換(Date/JSON)と例外処理がRepository側にある🧯
おまけ:この章の“今っぽさ”メモ🗓️✨
- TypeScriptは現在 5.9 系のリリースノートが公開されてるよ📘(TypeScript)
- Node.js は v24 が Active LTSとして扱われ、直近でもセキュリティリリースが出てる感じ🛡️(Node.js)
- Vite も継続的にアップデートされて、GitHub上で最新タグが追えるよ⚡(GitHub)
次の第15章は、このRepositoryを DI(差し替え注入) でさらに綺麗にして、テスト可能にしていくよ〜🔌🧪✨ そのまま続けていこっか?😊💕