第39章:interfaceで差し替え可能に(DIPの超入口)🔌🧩
ねらい🎯
- 「実装にベッタリ依存」してるコードを、interface(約束)経由にして差し替え可能にする✨
- テストで DB/APIなしでも動かせるようにする🧪🛟
- 「DIP(依存性逆転の原則)」を超やさしく体験する🌱
今日のキーワード🗝️
- DIP:上位(やりたいこと)が下位(道具の詳細)に直接依存しない🧠
- interface:必要な機能だけを表す「約束」📜
- 差し替え:本番はAPI、テストは偽物(Fake/Mock)にする🔁
- DI(依存性注入):差し替えを実現する具体テク(constructorで受け取るのが定番)📦
DIPの一言イメージはこれ👇 「やりたいこと(上位)は、道具のメーカー型番(実装)じゃなくて、**“機能の約束(interface)”**に話しかけよう」💬✨ (DIPの説明としてよくこう整理されます)(Strapi)
まず困る例😵(実装に直結してると…)
new ApiUserRepository()がクラスの中にあるfetch()がロジックの中に出てくる- テストしたいのに ネットワークが必要 → 遅い・不安定・再現しにくい🌧️
ビフォー:実装にベタ依存コード🧱💦
// ApiUserRepository.ts(低レイヤ:詳細)
export class ApiUserRepository {
async getById(id: string) {
const res = await fetch(`https://example.com/users/${id}`);
if (!res.ok) return null;
return (await res.json()) as { id: string; name: string };
}
}
// UserService.ts(高レイヤ:やりたいこと)
import { ApiUserRepository } from "./ApiUserRepository";
export class UserService {
private repo = new ApiUserRepository(); // ←ここが“直結”ポイント😵
async getDisplayName(userId: string) {
const user = await this.repo.getById(userId);
return user ? user.name.trim() : "Unknown";
}
}
この状態だと👇
- テストで
fetchが飛ぶ(外部に引っ張られる)📡 - 後から
DB版にしたいときもUserServiceを直すことになりがち🔧
アフター:interfaceを挟んで差し替え可能に✨


1) 「必要なことだけ」をinterfaceにする📜
ポイント:上位が欲しい機能だけを書く(下位の都合は持ち込まない)🎈
// UserRepository.ts(約束)
export type User = { id: string; name: string };
export interface UserRepository {
getById(id: string): Promise<User | null>;
}
2) 上位(UserService)はinterfaceだけを見る👀✨
// UserService.ts(高レイヤ:やりたいこと)
import type { UserRepository } from "./UserRepository";
export class UserService {
constructor(private readonly repo: UserRepository) {} // ←DI(注入)📦✨
async getDisplayName(userId: string) {
const user = await this.repo.getById(userId);
return user ? user.name.trim() : "Unknown";
}
}
3) 下位(API版)はinterfaceを“実装”する🔧
// ApiUserRepository.ts(低レイヤ:詳細)
import type { User, UserRepository } from "./UserRepository";
export class ApiUserRepository implements UserRepository {
constructor(private readonly baseUrl: string) {}
async getById(id: string): Promise<User | null> {
const res = await fetch(`${this.baseUrl}/users/${id}`);
if (!res.ok) return null;
return (await res.json()) as User;
}
}
4) “組み立てる場所”で本番の実装を選ぶ🧩
この「組み立て場所」をよく Composition Root(合成の根っこ)って呼びます🌳✨
// main.ts(組み立て)
import { UserService } from "./UserService";
import { ApiUserRepository } from "./ApiUserRepository";
const repo = new ApiUserRepository("https://example.com");
const service = new UserService(repo);
console.log(await service.getDisplayName("123"));
これで何が嬉しいの?🎁✨
UserServiceは API/DB/ファイルなどの詳細を知らない🙈- 実装を差し替えても
UserServiceはそのまま🧠✨ - テストでは 偽物Repo を渡せばOK🛟
テスト:Fake(偽物)でサクッと検証🧪🌸
Fake実装(インメモリ)
import type { User, UserRepository } from "./UserRepository";
export class InMemoryUserRepository implements UserRepository {
constructor(private readonly users: Map<string, User>) {}
async getById(id: string): Promise<User | null> {
return this.users.get(id) ?? null;
}
}
Vitestでテスト(外部なしで安定)✅
Vitestは vi でモック機能も提供しています(Vitest)(今回は分かりやすくFakeで!)
import { describe, it, expect } from "vitest";
import { UserService } from "./UserService";
import { InMemoryUserRepository } from "./InMemoryUserRepository";
describe("UserService", () => {
it("ユーザーがいないときは Unknown", async () => {
const repo = new InMemoryUserRepository(new Map());
const service = new UserService(repo);
await expect(service.getDisplayName("nope")).resolves.toBe("Unknown");
});
it("名前の前後空白を trim する", async () => {
const repo = new InMemoryUserRepository(
new Map([["1", { id: "1", name: " Alice " }]])
);
const service = new UserService(repo);
await expect(service.getDisplayName("1")).resolves.toBe("Alice");
});
});
“interface合ってる?”を強めにチェックする小ワザ🧷✨(satisfies)
「オブジェクトでFakeを書きたい!」とき、satisfies が便利です🎀
(satisfies は “型を満たしてるか検査しつつ、値の型を潰しすぎない” 演算子です)(TypeScript)
import type { UserRepository } from "./UserRepository";
const fakeRepo = {
async getById(id: string) {
if (id === "1") return { id: "1", name: "Alice" };
return null;
},
} satisfies UserRepository;
これを new UserService(fakeRepo) って渡せます👌✨
手順まとめ(迷子防止)👣🧭
- 直結してる依存を見つける(
new、fetch、fs、DB呼び出し)🔍 - 上位が本当に必要な操作だけを interface化📜
- 上位は interface型を受け取る(constructor注入)📦
- 下位は
implementsして詳細を担当🔧 - 組み立てる場所で「今日はどれを使う?」を決める🧩
ミニ課題✍️💖(3つ)
課題1:Loggerを差し替えできるようにしてみよう🪵✨
ConsoleLoggerに直結してるコードをLoggerinterface にして- テストでは
MemoryLogger(配列に溜める)に差し替え📦🧪
課題2:Repositoryのメソッド粒度を見直そう🔧
getByIdだけで足りる?saveが必要?- 「上位が欲しい操作」だけに絞れてる?🎯
課題3:差し替えの応用(Decorator)🎀
CachingUserRepositoryを作って- 中で本物Repoを呼びつつ、キャッシュしたら最強🧊✨ (これもinterfaceがあるから簡単に挟める!)
AI活用ポイント🤖✨(お願い方+チェック観点✅)
お願い方(例)💬
- 「このクラスが直接依存してる外部I/Oを列挙して、差し替えポイントを提案して」🔍
- 「この処理に必要な最小のinterfaceを提案して(メソッド名と戻り値も)」📜
- 「Fake実装とテストケース(正常/異常/境界)を作って」🧪
チェック観点✅(AIの提案を採用する前に!)
- interfaceが 大きすぎない?(“何でも屋”になってない?)🧯
- 上位が
Api...やDb...を import してない?🙅♀️ - テストが ネットワーク/DBなしで動く?🛟
- “組み立て場所”に依存の選択が集まってる?🧩
ちょい最新メモ📰✨(2026っぽい現場感)
- TypeScriptは 5.7/5.8/5.9 とリリースノートが続いていて、型チェックの改善がどんどん入っています(TypeScript)
- Node.js 側も LTS の世代が進むので、I/O周り(fetchやモジュール解決)を“詳細”として隔離しておくと移行がラクになりやすいです🔁(Node.js)
この章のゴール🏁✨
UserService が 「UserRepositoryという約束」だけに依存して、
本番はAPI、テストはFakeに スッと差し替えできたら大成功〜!🎉🔌