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

第8章:リファクタ② “境界”を作る(UIとロジックを分ける)🚪🧱

この章でできるようになること 🎯💖

  • UI(入力・表示)ロジック(判断・処理) を「線引き」できるようになる✂️✨
  • Query を 副作用ゼロ に近づけて、テストしやすい形にできる🧪🥳
  • “やりすぎ設計” にならない ちょうどいい分け方がわかる🧯🙂

2026年1月時点のメモ(最新リサーチ)🗓️🔎

  • TypeScript の npm latest は 5.9.3(2025-09-30 公開)だよ〜📦 (npm)
  • TypeScript **6.0 は 5.9 → 7.0 への“橋渡し”**として 2026年初頭リリース予定、という位置づけ(Microsoft 公式)🌉 (Microsoft for Developers)
  • Vite は GitHub リリース表示上 v7.3.1 が Latest(2026-01 中旬確認)⚡ (GitHub)
  • VS Code は 2026年1月の更新ページで 1.108 が available と表示されてるよ🧰 (Visual Studio Code)

この章の内容は TypeScript 5.9.x 前提でもそのままOK 👍✨(6.0/7.0 になっても “境界” の考え方は変わらないよ〜)


1) “境界”ってなに?🚪✨(超かんたん)

境界=「ここから先は UI の担当、こっちはロジックの担当ね!」って決める 境目のことだよ〜🙂💡

  • UI(外側):入力を受け取る、ボタン押下、DOM描画、アラート表示、フォーム操作…など 🎨🖱️
  • ロジック(内側):追加していい?どんなTodo作る?完了にしたらどうなる?並び順どうする?…など 🧠🧩

この境界を作ると、CQSが一気に守りやすくなるの! UI は副作用まみれでもOK。でも Query は副作用ゼロでいたいから、Query を UI から隔離しよ〜って話だよ🍃✨


2) なぜ UI とロジックを分けるの?🤔💥

UI とロジックが混ざってると、ありがちな事故が起きるよ〜😱

事故①:Query なのに副作用が混ざる 🧨

  • getTodos() の中で DOM を触ったり
  • ついでに localStorage 書いたり
  • ついでにログ送ったり → 「読むだけのはずが、何か起きてる」状態に…😇

事故②:テストが地獄 👿🧪

  • DOM が必要
  • ブラウザ環境が必要
  • 依存が多くて “入力→出力” の比較ができない → Query の良さ(テスト簡単)が消える〜😭

事故③:UI変更がロジックに波及して壊れる 🧟‍♀️

  • 見た目を少し変えただけで、ロジックが動かなくなる → “触ったら壊れる” アプリになる🥶

3) どこまで分ければOK?やりすぎ防止ライン🧯✨

この章は、まず レベル2 を目標にするのがちょうどいいよ〜🙂💖

境界レベル(おすすめ順)📈

  • レベル1:同ファイルで区切る(コメントの壁でもOK)🧱
  • レベル2:ファイル/フォルダを分ける(おすすめ) 📁✨
  • レベル3:外部依存(保存/通信)を“差し替え可能”にする 🔌(次以降の章で強くなる)

この章は レベル2中心+レベル3をチラ見くらいでいくね👀✨


4) “境界”の合言葉:依存の向きは一方通行➡️🧭

Boundary Wall

超ざっくりでOKなルール👇

  • 外側(UI)は内側(ロジック)を呼んでOK
  • 内側(ロジック)は外側(UI)を知らない(importしない)🙅‍♀️

イメージ👇

  • UI → ロジック ✅
  • ロジック → UI ❌

これが(初見でも大丈夫な)Dependency Rule の入口だよ〜🚪✨


5) ToDoミニアプリ:境界を作るリファクタ実演📝💖

ここからは「第7章までに分けた Command/Query」が まだUIと混ざってる想定で、境界を作るよ〜✂️✨

ゴールの構成(おすすめ)📁✨

src/
core/
todo.ts // 型・純粋関数(副作用なし)
todoStore.ts // 状態を持つ(Command側の“器”)
ui/
main.ts // DOM・イベント・描画(副作用だらけOK)
  • core/ロジック担当(内側) 🧠
  • ui/UI担当(外側) 🎨

6) core/todo.ts:まず “純粋ロジック” を作る🍃✨

ここはできるだけ 副作用なしでいくよ〜🧪🥳

// src/core/todo.ts
export type TodoId = string;

export type Todo = Readonly<{
id: TodoId;
title: string;
done: boolean;
createdAt: number; // epoch ms
}>;

// ✅ Query向け:表示用に整えるだけ(副作用なし)
export function sortTodosByCreatedAtDesc(todos: readonly Todo[]): Todo[] {
return [...todos].sort((a, b) => b.createdAt - a.createdAt);
}

// ✅ Commandの“中身”:配列をどう更新するか(副作用なし)
export function addTodo(
todos: readonly Todo[],
input: { id: TodoId; title: string; createdAt: number }
): Todo[] {
const title = input.title.trim();
if (title.length === 0) return [...todos]; // 空は無視(例)
const next: Todo = { id: input.id, title, done: false, createdAt: input.createdAt };
return [...todos, next];
}

export function completeTodo(todos: readonly Todo[], id: TodoId): Todo[] {
return todos.map(t => (t.id === id ? { ...t, done: true } : t));
}

ポイントだよ〜👇✨

  • core/todo.tsDOMもlocalStorageも触らない🙅‍♀️
  • todos を直接変更しないで、新しい配列を返す(地味に超大事)🧼✨
  • これだけでテストは “入力→出力” で終わるようになる🧪💕

7) core/todoStore.ts:状態(副作用)を “器” に閉じ込める📦🧩

UI が配列を直接いじり出すとまた混ざるから、状態は Store に持たせるのが楽だよ〜🙂✨

// src/core/todoStore.ts
import { addTodo, completeTodo, sortTodosByCreatedAtDesc, type Todo, type TodoId } from "./todo";

export class TodoStore {
private todos: Todo[] = [];

// Command ✅:状態を変える
add(title: string): void {
// “外部っぽいもの”はここで作って core/todo.ts に渡す
const id = crypto.randomUUID(); // ブラウザの副作用寄りAPI
const createdAt = Date.now(); // 時間も“揺れる”値
this.todos = addTodo(this.todos, { id, title, createdAt });
}

// Command ✅
complete(id: TodoId): void {
this.todos = completeTodo(this.todos, id);
}

// Query ✅:読むだけ(副作用なしの形で返す)
getAllSorted(): Todo[] {
return sortTodosByCreatedAtDesc(this.todos);
}
}

ここが “境界のコツ”💡

  • Date.now()crypto.randomUUID()ロジックの純度を落とす要素だから、Store側に寄せたよ〜🧠➡️📦
  • core/todo.ts の純度が上がって、テストしやすいまま保てる🧪✨

8) ui/main.ts:UIは “呼ぶだけ&描くだけ” にする🎨🖱️✨

UIは副作用の塊でOK!その代わり 判断を持ち込まないのが大事🙂💖

// src/ui/main.ts
import { TodoStore } from "../core/todoStore";
import type { Todo } from "../core/todo";

const store = new TodoStore();

const form = document.querySelector<HTMLFormElement>("#todo-form")!;
const input = document.querySelector<HTMLInputElement>("#todo-input")!;
const list = document.querySelector<HTMLUListElement>("#todo-list")!;

function render(todos: readonly Todo[]): void {
list.innerHTML = "";
for (const t of todos) {
const li = document.createElement("li");
li.dataset.id = t.id;

const label = document.createElement("span");
label.textContent = t.done ? `${t.title}` : `${t.title}`;

const btn = document.createElement("button");
btn.type = "button";
btn.textContent = t.done ? "完了済み💤" : "完了にする✅";
btn.disabled = t.done;

btn.addEventListener("click", () => {
store.complete(t.id); // ✅ Commandを呼ぶだけ
render(store.getAllSorted()); // ✅ Queryで取り直して描画
});

li.append(label, " ", btn);
list.append(li);
}
}

form.addEventListener("submit", (e) => {
e.preventDefault();
store.add(input.value); // ✅ Command
input.value = "";
render(store.getAllSorted()); // ✅ Query → 描画
});

// 初期描画
render(store.getAllSorted());

この形になると最高ポイント👇🥳✨

  • UI は store に命令するだけ(Command)
  • 表示は store から読むだけ(Query)
  • “更新後の一覧を返す” みたいな 混ぜ混ぜが自然に減る🔁💖

9) “境界”ができたかチェックするコツ👀✅

One Way Street

チェック①:core/ に DOM が出てきたら赤信号🚨

  • document, window, alertcore/ に出てきたら混ざってる!

チェック②:Query が何か保存/送信してたらアウト🙅‍♀️

  • localStorage.setItem
  • fetch でログ送信
  • DB更新(サーバー呼び出し) → それは Query じゃなくて Command 側へ!

チェック③:import の向きが逆になってない?➡️

  • core/ui/ を import してたら依存逆流😱
  • UI → core の一方通行を守ろ〜✨

10) やりすぎ防止🧯🙂「今日はここまででOK」ライン

境界を作るとき、こうなったら一旦ストップでOK👇

  • ファイル数が増えすぎて迷子📁😵‍💫
  • まだ機能が少ないのに DI/抽象化を盛りすぎた🌀
  • “綺麗” だけど、追加が遅くなった🐢

この章のゴールは 「UIとロジックが別れてて、CQSが守りやすい」 ことだよ〜💖 アーキテクチャ職人になるのは、もうちょい後でOK😌✨


11) ミニ演習(15〜30分)🧪⏰✨

演習A:フィルタ Query を core に追加してみよ〜🔎

  • getActiveTodos()(未完了だけ)
  • getDoneTodos()(完了だけ)

UI はボタンで切り替えて、Queryで取って render にする🎨✨

演習B:入力バリデーションをどこに置く?🧠

  • 空文字は無視
  • 50文字以上は禁止 これ、UIで弾く? coreで弾く? おすすめ:core側(addTodoの中)に寄せると、入口が増えても安全🙂💖

演習C:完了を “トグル” にしたい🔁

  • toggleTodo() を core に追加して、Store と UI を更新してみよ〜😆✨

12) AI活用コーナー🤖💡(そのまま投げてOK)

  • 「この ui/main.ts を、UIとロジックの境界が明確になるように分割して。core/ はDOM禁止で!」
  • 「この関数、Queryなのに副作用ありそう?怪しいところを指摘して、直して」
  • core/todo.ts のテストケースを3つ作って(入力→出力で)」🧪
  • 「依存の向き(UI→core)を壊してるimportがないかチェックして」👀

まとめ🌸✨

  • 境界=UIとロジックの担当分け🚪🧱
  • core は純度高めにすると、CQSが守りやすくてテストも楽🧪🥳
  • 依存の向きは UI → core の一方通行➡️✨
  • やりすぎず、まずは フォルダを分ける(レベル2) がちょうどいい📁💖

次の章(第9章)では、いよいよ Queryを副作用ゼロにする“チェック術” を、もっと具体的にやっていくよ〜🍃👀✨