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

第28章:重複のにおい(設計アラーム)👃🚨

重複へのアラーム

この章はね、**「あ、いま設計を良くするチャンス来た!」って気づけるようになる回だよ〜😊💕 TDDって、テストを書いて進むだけじゃなくて、“重複が見えた瞬間に設計が育つ”**のがめちゃ大事なんだ🧠🌱


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

  • 重複の種類を見分けられる(コピペだけじゃないよ!)👀✨
  • テストの重さ=設計のサインって分かるようになる⚖️
  • 改善を 最小→中→大 の3段階で提案&実行できる🪜✅
  • “DRYしすぎ事故”を避けながら、読み物として強くできる📘🛡️

🧠 本日時点の「道具」ちょいアップデート(超短く)🧪🆕

  • Vitest 4 がリリースされて、VS Code拡張のデバッグ体験も改善が入ってるよ🔎🐛 (Vitest)
  • Nodeは v24 がActive LTS 扱いで、セキュリティリリースも定期的に来るから、ちゃんと追うのが安心🛡️🪟 (Node.js)
  • TypeScriptは 5.9系 のリリースノートが公開されていて、Node向けオプションも整理が進んでるよ🧷 (TypeScript)
  • カバレッジを使うなら @vitest/coverage-v84.0.x が継続更新中だよ📈 (NPM)

(ここは章の主役じゃないので、必要な分だけね😉)


👃 “重複のにおい”ってなに?(コピペだけじゃない)🧠💡

「重複」はだいたいこの4タイプで出るよ👇

1) 📦 データの重複

同じオブジェクト生成、同じ配列、同じ入力…が何回も出てくるやつ。

2) 🔁 手順の重複

Arrangeが毎回長い、同じ準備を何度もやってる、みたいなやつ。

3) 🧭 ルール(知識)の重複 ← これが一番ヤバい🚨

同じ割引ルールが、実装のあちこちに散ってる 同じ分類(例:会員ランク)が、いろんなifで繰り返される → 変更に弱い設計になりやすい😵‍💫

4) ✅ Assertの重複

同じ期待を何度も書いてる / 失敗したとき何が壊れたか分かりにくい、みたいなやつ。


👃 重複が「設計アラーム」になる理由🔔

画像を挿入予定

TDDだと、最初はどうしても重複が出るのね🙂 でもそれは悪じゃなくて、

「次に育てるべき設計の形が見えてきたよ!」

っていうサインなの✨ 特に、テストのArrangeがつらいときは高確率で👇

  • 依存が混ざってる(ロジックとI/Oがごちゃ)🧩💥
  • 役割が大きすぎる(1クラスがなんでもやる)🧟‍♀️
  • ドメインの言葉(型/モデル)が足りない(string地獄)🏜️

🧪 ハンズオン:つらいテストを1つ“診断→改善案3つ”するよ🩺✨

🧪 お題(例):割引あり会計(軽め)🎟️🧾

※あなたの実案件や卒業制作のテストでもOKだよ!

😵‍💫 まずは「つらい」例(わざと)

  • Arrangeが毎回長い
  • 同じ数字や商品が何回も出る
  • ルールがテスト名から読み取れない
import { describe, it, expect } from "vitest";

type Item = { name: string; price: number; qty: number };
type Coupon = { code: "OFF10" | "OFF100"; };

function totalPrice(items: Item[], coupon?: Coupon): number {
const subtotal = items.reduce((sum, x) => sum + x.price * x.qty, 0);

// ルールがベタ書き(重複の温床)
if (!coupon) return subtotal;

if (coupon.code === "OFF10") return Math.floor(subtotal * 0.9);
if (coupon.code === "OFF100") return Math.max(0, subtotal - 100);

return subtotal;
}

describe("totalPrice", () => {
it("OFF10: 10%引きになる", () => {
const items: Item[] = [
{ name: "coffee", price: 300, qty: 2 },
{ name: "sandwich", price: 450, qty: 1 },
];
const coupon: Coupon = { code: "OFF10" };

const result = totalPrice(items, coupon);

expect(result).toBe(Math.floor((300 * 2 + 450) * 0.9));
});

it("OFF100: 100円引きになる", () => {
const items: Item[] = [
{ name: "coffee", price: 300, qty: 2 },
{ name: "sandwich", price: 450, qty: 1 },
];
const coupon: Coupon = { code: "OFF100" };

const result = totalPrice(items, coupon);

expect(result).toBe((300 * 2 + 450) - 100);
});
});

🩺 ステップ1:重複を「色分けメモ」する🖍️📝

上の例なら、こんな重複があるよ👇

  • 📦 itemsが丸ごと同じ(データ重複)
  • 🔁 Arrangeが毎回同じ(手順重複)
  • 🧭 割引ルールがベタ書き(知識重複の温床)
  • ✅ 期待値の式がその場で組み立て(読み物として弱い)

🪜 ステップ2:改善案を「最小→中→大」で3つ出す✨

✅ 改善案A(最小):テストのデータ重複だけ消す📦✨

ポイント:読みやすさ優先で、やりすぎない🙂

const sampleItems = (): Item[] => [
{ name: "coffee", price: 300, qty: 2 },
{ name: "sandwich", price: 450, qty: 1 },
];

describe("totalPrice", () => {
it("OFF10: 10%引きになる", () => {
const result = totalPrice(sampleItems(), { code: "OFF10" });
expect(result).toBe(945); // 1050の10%引き(床)
});

it("OFF100: 100円引きになる", () => {
const result = totalPrice(sampleItems(), { code: "OFF100" });
expect(result).toBe(950); // 1050-100
});
});

💡 ここでの狙い:

  • Arrangeが短くなる🧸✨
  • テストが「計算式」じゃなくて「仕様」に見える📘✅

✅ 改善案B(中):割引ルール(知識)を“名前”にする🧭🏷️

ポイント:“知識は1か所に集める”(変更に強くなる)💪✨

type CouponCode = "OFF10" | "OFF100";

function applyCoupon(subtotal: number, code: CouponCode): number {
if (code === "OFF10") return Math.floor(subtotal * 0.9);
return Math.max(0, subtotal - 100);
}

function totalPrice(items: Item[], coupon?: Coupon): number {
const subtotal = items.reduce((sum, x) => sum + x.price * x.qty, 0);
if (!coupon) return subtotal;
return applyCoupon(subtotal, coupon.code);
}

💡 ここでの狙い:

  • 「割引の知識」が applyCoupon に集まる📌
  • ルール追加(OFF200とか)が怖くなくなる➕🛡️

✅ 改善案C(大):テストの意図を“表”に寄せる(パラメータ化)🗂️🔁

ポイント:ケースが増える未来に強い✨(第22章とつながるよ)

describe("totalPrice coupon", () => {
const items = sampleItems(); // 固定でOK(仕様の前提)

const cases: Array<{ code: CouponCode; expected: number }> = [
{ code: "OFF10", expected: 945 },
{ code: "OFF100", expected: 950 },
];

it.each(cases)("$code: クーポン仕様どおりになる", ({ code, expected }) => {
expect(totalPrice(items, { code })).toBe(expected);
});
});

💡 ここでの狙い:

  • ケース追加が1行で済む🧠✨
  • “仕様の一覧”がそのままテストになる📘✅

⚠️ DRYしすぎ注意(テストでやりがちな事故)🚑💦

テストは「読み物」だから、重複を消しすぎると逆に読めなくなるの😵‍💫

🚫 ありがち

  • makeEverything() みたいな万能Factoryが増える
  • helperの中に意図が隠れて「何を検証してるか」が見えない
  • beforeEachで全部作って、各テストの前提が消える

✅ いい目安

  • 2回出たらメモ📝
  • 3回出たら抽出検討🧠
  • 抽出しても、テスト本文から「前提と期待」が読めること📘✨

🤖 AIの使い方(この章は相性◎)💞🤖

AIには「診断と案出し」をやらせると強いよ💪✨ (ただし、仕様決めは人間側ね😉)

🤖 プロンプト①:重複の分類(まず観察)👀

このVitestテストと実装を見て、「重複のにおい」を
(1)データ (2)手順 (3)知識(ルール) (4)assert の4分類で指摘して。
各分類につき、なぜ危険かを1行で。

🤖 プロンプト②:改善案を3段階(最小→中→大)🪜

このコードの重複に対して、改善案を
A:最小(安全・短時間)
B:中(設計が一段よくなる)
C:大(将来の拡張に強い)
の3つ出して。各案のメリデメも1行ずつ。

🤖 プロンプト③:DRYしすぎ警告🚨

提案したリファクタ案について、
「テストが読めなくなる危険」「抽象化しすぎの危険」がある点を列挙して。
避けるルールも提案して。

✅ チェック(合格ライン)🎓✅

  • 重複を4分類で説明できる👃
  • 改善案を「最小→中→大」で3つ言える🪜
  • 最小案を実際にやって、テストが短く読みやすくなった📘✨
  • ルール(知識)の重複が1か所に寄った🧭📌
  • helperを作っても、テスト本文から意図が読める🙂✅

🧾 提出物(この章の“手を動かす”のゴール)🎁✨

  1. 「つらいテスト」1つ選ぶ
  2. 重複を4分類でメモ📝
  3. 改善案A/B/Cを文章で書く(各3行くらいでOK)💬
  4. まずAを実装してコミット✅ (余裕あればBまで!)

次の第29章は「リファクタ安全運転(小さく)」だから、 この章で出した改善案を “壊さず進める順番” に落としていくよ🛡️✨

「つらいテスト」の題材、もし今ちょうど手元にあるなら、そのコード貼ってくれたら、いっしょに“重複診断→A/B/C案”まで一気に作るよ〜👃🚨💕