第81章:ルールの分離:if地獄を見つける👀
この章はひとことで言うと… **「ルール(条件)が増えたとき、コードが“爆発”する前兆=if地獄を見抜く回」**だよ〜!💣✨ 次の章(Specification)に進むために、まず “何が複雑なのか”をちゃんと見える化します📝🔎
ちなみに、2026-02-07時点で、npm上のTypeScriptの最新版表示は 5.9.3 になってるよ(安定版として扱いやすい)📌✨ (NPM) (「TypeScript 5.9」の公式Release Notesページもあるよ🧾) (typescriptlang.org)
1) “if地獄”ってなに?どうして起きるの?😵💫🌪️
✅ if地獄の正体
条件(if)が増えすぎて、変更が怖くなる状態のことだよ🥺💦 たとえば割引やキャンペーンって、こうなるでしょ?
- 新キャンペーン追加🎉
- 既存キャンペーンの条件が微修正🪛
- 併用不可ルールが増える🚫
- 「例外」が増える(でも仕様書は増えない)📄😇
そして気づくと、コードがこうなる👇
- 条件が長い(&& と || がぐちゃぐちゃ)🫠
- 同じ条件がコピペで散らばる📎
- どれが“業務ルール”で、どれが“手順”か分からない🤯
- テストが書けない/壊れる💥
2) 例:カフェ注文に“割引地獄”を発生させてみる☕🏷️(わざとね!)
ここでは「割引ルール」が増える未来を想像して、あえて地獄を作るよ😂🧨
🎯 ありがちな割引ルール(例)
- 学割:学生なら10%OFF🎓
- 平日限定:平日だけ適用📅
- ハッピーアワー:15:00〜17:00なら15%OFF⏰
- 初回クーポン:合計1500円以上で500円引き🎫
- VIP:20%OFF(ただし他と併用不可👑🚫)
- 「一番お得な1つだけ適用」みたいなルールもありがち💰✨
😇 こういうコードが生まれやすい(例)
(※ “ダメ例”として見てね!)
type Order = {
totalYen: number;
isStudent: boolean;
isVip: boolean;
isFirstOrder: boolean;
hasCoupon: boolean;
dayOfWeek: "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun";
hour: number; // 0-23
};
export function calculateDiscountYen(order: Order): number {
let discount = 0;
// VIPは強い(併用不可)
if (order.isVip) {
discount = Math.floor(order.totalYen * 0.2);
} else {
// 学割(平日限定)
if (order.isStudent) {
if (
order.dayOfWeek === "Mon" ||
order.dayOfWeek === "Tue" ||
order.dayOfWeek === "Wed" ||
order.dayOfWeek === "Thu" ||
order.dayOfWeek === "Fri"
) {
discount = Math.floor(order.totalYen * 0.1);
}
}
// ハッピーアワー(さらにお得?)
if (order.hour >= 15 && order.hour < 17) {
const happy = Math.floor(order.totalYen * 0.15);
if (happy > discount) discount = happy;
}
// 初回クーポン
if (order.isFirstOrder && order.hasCoupon) {
if (order.totalYen >= 1500) {
if (500 > discount) discount = 500;
}
}
}
// 念のため上限(よくある)
if (discount > order.totalYen) discount = order.totalYen;
return discount;
}
💥 これが“地獄の入口”な理由
この時点でもう、未来が見える…👀🧨
- 条件が増えるたびにこの関数が太る🐷
- 「平日判定」みたいな条件が他でも必要になってコピペされる📎
- “一番お得なやつだけ”の比較ロジックが散らばる⚖️
- ルール同士の優先順位が“ifの順番”に埋まってしまう🧩😱
- 「この割引、なぜ適用されないの?」が説明できない💬🫥
3) if地獄の“見分け方”チェックリスト✅👀
当てはまるほど、「ルールを分離する道具(Specification/Policyなど)」が必要だよ🧰✨
🧨 におい(コードの症状)
- ✅ ifのネストが深い(3段くらいで警戒)🕳️
- ✅ 条件式が長い(&& || が多い、カッコが多い)🧠💦
- ✅ 同じ判定があちこちにある(平日判定、会員判定など)📎
- ✅ “例外だけ特別扱い”が増える(VIPだけ別、初回だけ別…)👑🎫
- ✅ 変更のたびに「どこを直せば?」って探検が始まる🧭😵💫
- ✅ 仕様を説明するとき、コードを開かないと話せない📖💥
🧪 におい(テストの症状)
- ✅ テストが「全パターン網羅できない」感じになる🧪😇
- ✅ 1つのテストで準備が重い(データが大きい)🧱
- ✅ 失敗時に「どの条件が原因?」が分からない🔍💦
4) “見える化”のコツ:ルールを箇条書きに分解する📝✨
ここがこの章のメイン作業だよ〜!💪💖 やることはシンプル:
🎯 手順A:コードから「ルール文」を抜く
さっきの例なら、こんなふうに書き出す:
- ルール1:VIPなら20%引き(他と併用不可)👑🚫
- ルール2:学生かつ平日なら10%引き🎓📅
- ルール3:15〜17時なら15%引き⏰
- ルール4:初回かつクーポンありかつ1500円以上なら500円引き🎫
- ルール5:複数ある場合は「最大割引1つだけ」💰✨
- ルール6:割引は合計を超えない🧱
✅ ポイント:まず“自然言語”に戻すの! コードのまま考えると、ifの順番に引っ張られて迷子になるよ🧭🥺
🎯 手順B:各ルールを「条件」と「結果」に分ける
- 条件(いつ?)→ 学生 && 平日
- 結果(どうする?)→ 10%引く
これを分けるだけで、次章のSpecification/Policyがすごく作りやすくなるよ🔎📄✨
5) “まずは軽く分離”してみる:名前をつけて外に出す🏷️➡️📦
いきなりSpecificationに行く前に、最小の改善をやってみよ〜!💡 コツは 「条件に名前をつける」 だけ!それだけで世界が変わる🌍✨
function isWeekday(day: Order["dayOfWeek"]): boolean {
return day === "Mon" || day === "Tue" || day === "Wed" || day === "Thu" || day === "Fri";
}
function isHappyHour(hour: number): boolean {
return hour >= 15 && hour < 17;
}
function canUseFirstCoupon(order: Order): boolean {
return order.isFirstOrder && order.hasCoupon && order.totalYen >= 1500;
}
export function calculateDiscountYen(order: Order): number {
if (order.isVip) {
return Math.min(Math.floor(order.totalYen * 0.2), order.totalYen);
}
const candidates: number[] = [];
if (order.isStudent && isWeekday(order.dayOfWeek)) {
candidates.push(Math.floor(order.totalYen * 0.1));
}
if (isHappyHour(order.hour)) {
candidates.push(Math.floor(order.totalYen * 0.15));
}
if (canUseFirstCoupon(order)) {
candidates.push(500);
}
const best = candidates.length === 0 ? 0 : Math.max(...candidates);
return Math.min(best, order.totalYen);
}
✅ この時点で得してること🎉
- 条件が「読める」📖✨(isWeekday / isHappyHour って意味がそのまま)
- ルールごとにテストが書ける🧪💪
- ルール追加が “関数を1個足す” に近づく🧩➡️🧩
- 次章の Specificationの形(isSatisfiedBy) に超近い😍
6) テストの勝ち筋:ルール単位で小さく検証する🧪💎
「割引計算の全部」をテストしようとするとしんどい😇 だから、条件関数を先にテストするのが楽だよ〜!
import { describe, it, expect } from "vitest";
describe("isWeekday", () => {
it("Monは平日", () => {
expect(isWeekday("Mon")).toBe(true);
});
it("Satは平日じゃない", () => {
expect(isWeekday("Sat")).toBe(false);
});
});
describe("canUseFirstCoupon", () => {
it("初回+クーポン+1500円以上ならOK", () => {
const order = {
totalYen: 1500,
isStudent: false,
isVip: false,
isFirstOrder: true,
hasCoupon: true,
dayOfWeek: "Mon",
hour: 12,
} satisfies Order;
expect(canUseFirstCoupon(order)).toBe(true);
});
});
ここで使ってる satisfies は「型に合ってるかチェックしつつ、型を変えない」便利機能だよ🧡 テストデータ作る時、地味に事故が減る✨ (typescriptlang.org)
7) AIの使いどころ(この章は“壁打ち”が最強🤖🫶)
この章は、AIがめちゃくちゃ相性いいよ〜!😍✨ おすすめの使い方だけ置いとくね(そのまま投げてOK)📮💕
🧠 プロンプト例①:ルール抽出(自然言語化)
- 「この関数の条件分岐を、業務ルールとして箇条書きにして。条件と結果を分けて。」
🧠 プロンプト例②:重複発見
- 「このコード内で重複している条件(同じ意味の判定)を見つけて、共通関数名を提案して。」
🧠 プロンプト例③:テスト観点生成
- 「この割引ルールの境界値と例外ケースをGiven/When/Thenで10個提案して。」
🧠 プロンプト例④:仕様の矛盾チェック
- 「ルール同士の矛盾(同時に成り立たない、優先順位が必要など)を指摘して。」
✅ ただし注意:AIは“仕様の正しさ”は保証できないから、最後は自分の言葉でOK/NG決めよ〜!🫶🧠
8) まとめ:この章でできるようになったこと🎒✨
- ✅ if地獄の“におい”を嗅ぎ分けられる👃🧨
- ✅ ルールを「条件」と「結果」に分解できる📝
- ✅ 条件に名前をつけて、まずは関数に逃がせる🏷️➡️📦
- ✅ ルール単位でテストを書ける🧪💎
そして次章(第82章)でついに… **条件をオブジェクト化する(Specification)**に入るよ〜!🔎📄✨ この章の「名前をつけた条件関数」が、そのまま材料になる😍
9) ミニ宿題(15〜30分)🧁⏳
宿題A:if地獄センサーON👀
自分のコード(または例題)で、以下を探してみて👇
- ネスト深めのif
- 長い条件式
- 重複条件
宿題B:ルールを5個、自然言語に戻す📝
「条件」と「結果」に分けて箇条書き!✨
宿題C:条件を2個だけ関数にしてみる🏷️
例:isWeekday / isHappyHour みたいにね💕
必要なら、あなたの現行コード(割引やキャンペーン周り)を貼ってくれたら、**“どこがif地獄の芽か”**を一緒にマーキングして、**ルールの分解(条件/結果)**まで一気にやるよ👀🧡