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

第9章:ライトな3層設計(UI / Application / Domain)🍰✨

2026/01/11 時点メモ📝

  • npm の TypeScript 最新安定版は 5.9.3(2025/09/30 公開)です。 (npm)
  • TypeScript チームは TypeScript 6.0 を「最後のJavaScript実装のメジャー」、**5.9 ↔ 7.0 をつなぐ“ブリッジ”**と説明しています。 (Microsoft for Developers)
  • VS Code の直近リリースは 1.108(2026/01/08) です。 (Visual Studio Code)
  • Node.js は v24 が Active LTS(サイト上の “Latest LTS” 表記も v24.12.0)です。 (Node.js)

9-1. 3層ってなに?ざっくり一発で掴む🌟

3 Layers

3層(ライト版)は、こう分けます👇

  • UI層:表示・入力・イベントだけ 🖥️⌨️
  • Application層:やりたいことの流れ(ユースケース)🧭
  • Domain層:ルールの中心(変更に強い核)🛡️💎

イメージはこれっ👇

[ UI ]  画面/入力/クリック/HTTP/CLI
↓ 呼び出す
[ Application ] ユースケース(申込する・注文する・支払う)
↓ ルールを使う
[ Domain ] ルール(割引、上限、状態遷移、検証、計算)

この分け方が効く理由はシンプルで、

  • UIはUIの都合で変わる(見た目・入力・画面遷移)🖼️
  • アプリは流れが変わる(手順・ユースケース・承認フロー)🧭
  • ドメインはルールが変わる(計算・制約・状態)📏

👉 変わる理由が違うものは混ぜない=SoCの超実践形です🧁✨


9-2. まず「置き場迷子」をなくす!3層の責務まとめ✅

UI層🖥️

やること:

  • 入力を集める(フォーム、ボタン、CLIの引数)⌨️
  • 表示する(画面描画、ログ、レスポンス)📣
  • 変換する(文字列→数値 みたいな“浅い変換”)🔁

やらないこと:

  • 「学生なら10%引き」みたいな業務ルール
  • 「申込は先着100名まで」みたいな制約
  • 「申込処理の手順(保存→通知→…)」をゴリゴリ書く❌

Application層🧭

やること:

  • ユースケースの順番を管理(例:申込 → 料金計算 → 保存 → 通知)🧩
  • Domainを呼び出して、結果をまとめる🍱
  • 依存(DB/通信など)は **“ここから外に出さない”**意識(詳細は後章で強化)🚪

やらないこと:

  • 料金計算の中身(式)をここにベタ書き❌ → それはDomainの仕事🛡️

Domain層🛡️

やること:

  • ルール(計算、検証、状態遷移)📏
  • 安全な型(第10章につながる💖)
  • できれば 純粋関数中心(第7章の「副作用と分ける」をここで活かす✨)

やらないこと:

  • fetch/DB/ファイル/日時取得を直で叩く❌(副作用は外へ)⚡

9-3. “ライト”ってどれくらいライト?😌🍰

ここでの3層は、ガチなクリーンアーキテクチャをいきなりやる話じゃないです🙅‍♀️💦 目標はこれ👇

  • フォルダ3つに分けるだけで「混ざり」を減らす
  • import の向きを固定して、事故を減らす
  • 後からDIP/DIに進める“形”を先に作る

9-4. 例題:学園イベント申込🎓🌸(この章の主役)

仕様(かわいめ&リアル寄り)✨

  • イベント参加費:通常 2,000円 💰
  • 学生は 20%割引 🎀
  • 定員は 100名 🧑‍🤝‍🧑
  • すでに満員なら申込できない 😵‍💫

9-5. フォルダ構成(最小)📁✨

src/
ui/
main.ts // 入口(CLIでもWebでもここがUI)
application/
applyForEvent.ts // ユースケース
domain/
pricing.ts // 料金計算ルール
capacity.ts // 定員ルール
types.ts // ドメイン型(軽く)

import のルール(超大事)🚦

  • UI → Application / Domain を呼んでOK✅
  • Application → Domain を呼んでOK✅
  • Domain → ほかに依存しない(基本)✅

これだけで「ごちゃ混ぜ」が一気に減ります😳✨


9-6. 実装してみよっ✨(コードは小さく、でも分離はハッキリ)

Domain:料金計算ルール🛡️💰

// src/domain/types.ts
export type Yen = number;

export type Applicant = {
isStudent: boolean;
};

export type PricingRule = {
baseFee: Yen;
studentDiscountRate: number; // 0.2 = 20%
};
// src/domain/pricing.ts
import type { Applicant, PricingRule, Yen } from "./types";

export function calculateFee(applicant: Applicant, rule: PricingRule): Yen {
const fee = rule.baseFee;
if (!applicant.isStudent) return fee;

const discounted = fee * (1 - rule.studentDiscountRate);
// 端数は四捨五入(例として)
return Math.round(discounted);
}

👉 ここは UIもDBも知らない、ただのルールの世界🌍🛡️

Domain:定員ルール🛡️🧑‍🤝‍🧑

// src/domain/capacity.ts
export function ensureCapacity(currentCount: number, limit: number): void {
if (currentCount >= limit) {
throw new Error("満員のため申込できません");
}
}

Application:申込ユースケース🧭✨

// src/application/applyForEvent.ts
import { calculateFee } from "../domain/pricing";
import { ensureCapacity } from "../domain/capacity";
import type { Applicant, PricingRule, Yen } from "../domain/types";

export type ApplyInput = {
applicant: Applicant;
};

export type ApplyResult = {
fee: Yen;
message: string;
};

export type EventState = {
currentCount: number;
limit: number;
};

export function applyForEvent(
input: ApplyInput,
state: EventState,
rule: PricingRule
): ApplyResult {
// 1) 定員チェック
ensureCapacity(state.currentCount, state.limit);

// 2) 料金計算
const fee = calculateFee(input.applicant, rule);

// 3) 本当はここで保存/通知など(第12〜14章で強化するよ💖)
// 今回は「成功したことにする」だけ
return {
fee,
message: `申込OK!参加費は ${fee} 円です✨`,
};
}

👉 Applicationは 手順の司令塔。 ルールの中身はDomainに任せて、ここは“流れ”に集中🧭✨


UI:入口(CLIっぽく簡単に)🖥️🎀

// src/ui/main.ts
import { applyForEvent } from "../application/applyForEvent";

function parseIsStudent(arg: string | undefined): boolean {
return arg === "student";
}

function main(): void {
// 例: node dist/ui/main.js student
const isStudent = parseIsStudent(process.argv[2]);

const result = applyForEvent(
{ applicant: { isStudent } },
{ currentCount: 99, limit: 100 },
{ baseFee: 2000, studentDiscountRate: 0.2 }
);

console.log(result.message);
}

main();

UIがやってるのは「入力→呼ぶ→表示」だけ🎯 これが気持ちいい分離です〜!🥰✨


9-7. 「どこに置く?」迷ったときの判定表✅🧭

  • 「画面の文言・表示形式を変えたい」→ UI 🖥️
  • 「申込の手順が変わる(事前確認→申込→決済、みたいな)」→ Application 🧭
  • 「学生割引率が変わる」「定員ルールが変わる」→ Domain 🛡️
  • 「DBに保存したい」「APIを叩きたい」→ いったん Application側の端っこ(詳細は後章で“境界”を作るよ)🚪✨

9-8. AI活用のコツ🤖💖(Copilot/Codexが強い場面)

① 分離案を3つ出させる🎁

プロンプト例:

  • 「この機能を UI / Application / Domain に分けて、責務とファイル案を3パターン出して」
  • 「Domainに置くべきルール候補を箇条書きにして」

👉 3案出させると、“置き場迷子”が激減します😳✨

② Applicationの“手順”だけ書かせる🧭

プロンプト例:

  • 「applyForEvent のユースケース手順だけ擬似コードで(Domain呼び出し前提で)」

③ Domainは“純粋関数”縛りで生成させる🧼✨

プロンプト例:

  • 「副作用なし、引数と戻り値だけの関数で料金計算を書いて」

9-9. ミニ演習🎮✨(5〜10分)

お題🎓

「早割」ルールを追加したい!⏰✨

  • イベントの7日前までの申込なら 10%引き
  • 学生割引と併用OK(順番は「学生→早割」でも「早割→学生」でもOK、今回は合計割引率でOKにしよ)

✅ 質問:この変更、どの層に入れる?どのファイル?

答え:基本は Domain(料金計算ルール)🛡️💰 理由:割引の中身は“業務ルール”だから📏✨

ヒント実装イメージ:

  • Domainの calculateFeeappliedAteventDate を渡す
  • もしくは「割引判定関数」を分ける(読みやすさUP)📚

9-10. よくある失敗あるある😇💥(先に潰そ)

  • Domainにconsole.logが入る → UIの関心が混ざってる🥲
  • Applicationに計算式がベタ書き → ルール変更で地獄😵‍💫
  • UIが“例外の文言”まで全部作る → ルールの意味がUIに漏れる😇
  • とりあえずutilに入れる → “便利箱”が爆発する📦💥(第8章の罠!)

9-11. まとめ🍰✨(ここだけ覚えて!)

  • 3層は 「変わる理由」で分ける 🧁
  • UI=入出力、Application=手順、Domain=ルール🖥️🧭🛡️
  • importの向きを固定すると、SoCが“維持”できる🚦✨
  • 次章(第10章)で 型を使って境界をもっと硬くするよ〜!🧠🛡️💖