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

第6章:まず失敗例を見る(外部DTOが侵食する地獄)🕳️😵

この章のゴール🎯✨

  • 「外部DTOをそのまま内側で使う」と何がツラいのかを、コードで体感できるようになる🧠💥
  • ツラさを「言語化」して、次章以降のACL実装がスッと入る状態になる🧱🚪✨
  • “どこからが侵食?”を見抜けるようになる👀🧪

まずは超ミニの題材🎓🍱

学内アプリ(例:学食ポイント)で「学生のポイント残高」を見て、学食を買えるか判定する…みたいなイメージね🍙✨

  • 外部サービス:学生情報API(クセ強め)👻
  • 内側:学食ポイントという“自分たちのルール”で考えたい📘✨
  • なのに…外部DTOをそのまま内側で使うと…地獄が始まる😇🔥

2026年1月時点の“今どき”メモ🧰🆕

  • Node.js は v24(Krypton)が Active LTS、v25 が Current になってるよ📌🟢 (Node.js)
  • Node.js の fetch は v21 で安定(stable)扱いになってるよ🌐✅ (Node.js)
  • TypeScript の最新安定タグは 5.9.3 が “Latest” として出てるよ🟦✨ (GitHub)

(※この章の主役は“設計の痛み”だから、環境はサクッとでOK👌)


1) 外部DTOをそのまま使う「わざと悪い例」😈🧪

外部APIのレスポンス(例)📦

外部が返すJSONは、こんな “外の都合” が詰まってる想定👇

  • 変な命名(stu_id / nm_kj / pt…)🌀
  • 区分が謎コード("1" とか "2" とか)🔤❓
  • 数値っぽいのに文字列(ptが"1200")😵‍💫
  • null が普通に来る(名前がnullとか)🫠

2) まずはDTO型を作る(外の形そのまま)🧾

// src/external/studentDirectoryDto.ts
export type StudentDirectoryDto = {
stu_id: string; // 学籍IDっぽい
stu_kbn: "1" | "2"; // 謎の区分コード(例:1=学部, 2=院)
nm_kj: string | null; // 漢字氏名(null来ることも…)
pt: string; // ポイント(なぜか文字列)
upd: string; // 更新日時(ISO文字列)
};

3) “外部クライアント”も作る(でもこの時点で怪しい)🌐😇

// src/external/studentDirectoryClient.ts
import { StudentDirectoryDto } from "./studentDirectoryDto";

export async function fetchStudent(stuId: string): Promise<StudentDirectoryDto> {
const res = await fetch(`https://example.edu/api/students/${stuId}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);

// 🚨 ここが最初の落とし穴:実体が正しいか検証せず「型だと信じる」
return (await res.json()) as StudentDirectoryDto;
}

ここ、TypeScriptだと「as で型付け」できちゃうから、安心した気になりやすいのが罠😵‍💫🪤 (でも実際のJSONが違っても、コンパイルは通る…)


4) 内側(ユースケース)がDTOを直で食べる🍽️💀

// src/usecases/canBuyLunch.ts
import { StudentDirectoryDto } from "../external/studentDirectoryDto";

export function canBuyLunch(dto: StudentDirectoryDto, lunchPrice: number): boolean {
// “pt”って何…?ってなる🌀
const points = Number(dto.pt);

// “stu_kbn”って何…?ってなる🌀
if (dto.stu_kbn === "2") return false; // 例:院生は学食補助対象外…とか(外部コードに依存)

// nullチェックが内側に侵食🫠
if (dto.nm_kj === null) return false;

// 数値変換に失敗すると NaN 地獄😇
return points >= lunchPrice;
}

この時点で起きてる“侵食”🧟‍♀️🧠

  • 内側が外側の言葉(stu_kbn / nm_kj / pt)で埋まる🌀
  • “コード値の意味”が内側に混ざる🔤➡️📘(翻訳なし)
  • null対応・文字列数値変換など、外部の後始末が内側に来る🧹💦

5) 「変更に弱い」を一発で体感する💥📉

パターンA:外部が pt を points に変更した(ありがち)🔁

外部が仕様変更で JSON をこう変えたとする👇

  • 旧:pt
  • 新:points

でも、さっきの fetchStudent は「検証なしで as」してるから…

  • TypeScriptは怒らない😇
  • 実行すると dto.pt は undefined
  • Number(undefined) は NaN
  • 判定が全部おかしくなる🫠💥

つまり:型があっても“境界で検証しない”と守れないってこと🧱🚪⚠️


パターンB:謎コードが増える("3"が来た)🔤👻

stu_kbn が "1" | "2" だと思ってたのに、ある日 "3" が来る😇

  • 型的には「想定外」だけど
  • 実体として来たら普通に動いてしまう(そして意図しない分岐へ)🌀

こういう “外部の仕様変更” は、こちらの努力と無関係に突然来るのが怖いんだよね😱🌩️


6) 「テストがつらい」を体感する🧪💦

内側の関数 canBuyLunch をテストしたいだけなのに、DTOを丸ごと作らされる😭

// test/canBuyLunch.test.ts
import { describe, it, expect } from "vitest";
import { canBuyLunch } from "../src/usecases/canBuyLunch";

describe("canBuyLunch", () => {
it("ポイントが足りていれば true", () => {
const dto = {
stu_id: "A0123",
stu_kbn: "1",
nm_kj: "田中 花子",
pt: "500",
upd: "2026-01-29T12:00:00+09:00",
};

expect(canBuyLunch(dto, 480)).toBe(true);
});

it("nm_kj が null だと false(内側が外の事情に引っ張られてる)", () => {
const dto = {
stu_id: "A0123",
stu_kbn: "1",
nm_kj: null,
pt: "500",
upd: "2026-01-29T12:00:00+09:00",
};

expect(canBuyLunch(dto, 480)).toBe(false);
});
});

何がつらいの?😵‍💫

  • テストしたいのは「学食を買えるか」なのに、外部項目(stu_id, upd…)まで毎回書く羽目📝💦
  • DTOが肥大化すると、テストが「作業」になる🪨
  • しかも外部仕様が変わると、内側のテストがまとめて壊れる💥🧨

7) “侵食してるかどうか”の見分け方チェック✅👀

内側のコードで、こういうのが見えたら黄色信号🚥💛

  • 変な略語・スネークケースが出てくる(pt / nm_kj / stu_kbn)🐍🌀
  • "1" や "2" みたいな謎コードで分岐してる🔤❓
  • Number(dto.pt) みたいな “外部の後始末” が内側にある🧹
  • null対応が内側のあちこちに散ってる🫠
  • テストデータが「外部レスポンスの工作」になってる📦🧪

8) ここでACLの必要性が“腹落ち”するポイント🧱🛡️✨

外部と内側は、そもそも「別の言語(別の世界)」なんだよね🌍🗣️ 混ぜると、語彙が混ざってコードがぐちゃぐちゃになる…って話が有名🍝💥 (martinfowler.com)

だから境界に 翻訳レイヤー(ACL) を置いて、

  • 外部の都合 ↔ 内側の都合 を変換する🧾➡️📘
  • モデル同士を翻訳して腐敗を防ぐ🧼🧱

っていうのがACLの基本アイデアだよ✨ (microservices.io)


9) AI活用(この章の“悪い例”作りにも使える)🤖🧠✨

Copilot/Codex向けの例プロンプト💬

  • 「学生情報APIのDTO型を、スネークケースのフィールドで作って」🧾
  • 「DTOをそのまま受け取って学食購入可否を判定する関数を雑に作って」😈
  • 「DTOの pt が undefined になるケースを作って、バグる様子を再現するテストを書いて」🧪💥

⚠️ ただし、AIが作る “雑な変換” はこの章ではわざとOKだけど、次章以降は「内側の言葉」を人間が監督して守るよ🛡️✨


10) まとめ:この章でわかった“地獄ポイント”🔥🕳️

イメージ:外部DTOによる内側の侵食

  • DTOを内側で直に使うと、内側が外部の語彙で汚染される🌀
  • 変更に弱くなる(外部変更 → 内側崩壊)💥
  • 「as」で型を信じるだけだと、実体の変更を止められない😇
  • テストが外部レスポンス工作になって、しんどくなる🧪💦

次章からは、ここで出た痛みをぜんぶ回収していくよ🧼🧱✨