📝 注記
私は日本語が得意ではありません。この記事はAIの翻訳サポートを受けて書いています。ご了承ください。
📖 目次
- 問題の提示 – どんな時にこのテクニックが必要か
- 悪い例 – まずはダメなコードを見せる
- 良い例 – TypeScriptの高度機能で解決する
- DIコンテナの比較 – ライブラリ選定ガイド
- 課題 – シニア向けのチャレンジ問題
- まとめ
1. 問題の提示 – どんな時にこのテクニックが必要か
あなたはECサイトのバックエンドを構築しています。以下のようなコードを書いていませんか?
class OrderService {
private db: Database;
private logger: Logger;
private emailService: EmailService;
private paymentGateway: PaymentGateway;
constructor() {
// ❌ 直接依存関係をnewしている
this.db = new Database(config);
this.logger = new Logger();
this.emailService = new EmailService();
this.paymentGateway = new PaymentGateway();
}
async createOrder(order: Order) {
this.logger.info("Creating order");
await this.db.save(order);
await this.paymentGateway.charge(order.total);
await this.emailService.sendConfirmation(order);
}
}
問題点:
- クラスが自分自身の依存関係を直接インスタンス化している
- 単体テストが困難(モックに差し替えられない)
- 依存関係の変更がコード全体に波及する
- クラス間の結合度が高く、再利用性が低い
問いかけ:
どうすればクラスを依存関係から解放し、テスタブルで疎結合な設計を実現できるでしょうか?
2. 悪い例 – まずはダメなコードを見せる
2.1 直接インスタンス化の弊害
// ❌ 直接インスタンス化
class UserService {
private db: PostgreSQLDatabase;
private logger: FileLogger;
constructor() {
// これらの依存関係を差し替えられない
this.db = new PostgreSQLDatabase({ host: "localhost", port: 5432 });
this.logger = new FileLogger("/var/log/app.log");
}
async getUser(id: string) {
this.logger.info(`Fetching user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
// ^ SQLインジェクションの危険性も
}
}
// テストが不可能に近い
describe("UserService", () => {
it("should get user", async () => {
const service = new UserService();
// ❌ 実際のデータベースに接続してしまう
// ❌ ログファイルも実際に作成される
const user = await service.getUser("1");
});
});
2.2 手動依存性注入(サービスロケーターパターン)
// ❌ サービスロケーター(アンチパターンになりがち)
class ServiceLocator {
private static services: Map<string, any> = new Map();
static set(name: string, service: any) {
this.services.set(name, service);
}
static get(name: string) {
const service = this.services.get(name);
if (!service) throw new Error(`Service ${name} not found`);
return service;
}
}
ServiceLocator.set("database", new PostgreSQLDatabase());
ServiceLocator.set("logger", new FileLogger());
class UserService {
private db: Database;
private logger: Logger;
constructor() {
// まだServiceLocatorに依存している
this.db = ServiceLocator.get("database");
this.logger = ServiceLocator.get("logger");
}
}
// 問題点:
// 1. どの依存関係が必要か型から読み取れない
// 2. グローバルな状態を持つ
// 3. テスト時に毎回登録し直す必要がある
なぜ悪いのか:
| 問題 | 説明 |
|---|---|
| 高結合 | クラスが具象クラスに直接依存 |
| テスト困難 | モックに差し替えられない |
| 隠れた依存関係 | コンストラクタを見ただけでは分からない |
| 単一責任違反 | オブジェクト生成の責任も負っている |
3. 良い例 – TypeScriptの高度機能で解決する
3.1 コンストラクタインジェクション(手動DI)
// ✅ 依存関係をコンストラクタで受け取る
interface Database {
query(sql: string, params?: any[]): Promise<any>;
}
interface Logger {
info(message: string): void;
error(message: string): void;
}
class UserService {
constructor(
private db: Database,
private logger: Logger
) {}
async getUser(id: string): Promise<User> {
this.logger.info(`Fetching user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = $1`, [id]);
}
}
// 実際の使用
const db = new PostgreSQLDatabase(config);
const logger = new ConsoleLogger();
const userService = new UserService(db, logger);
// テストではモックを注入
class MockDatabase implements Database {
async query(sql: string): Promise<any> {
return [{ id: "1", name: "Test User" }];
}
}
const mockDb = new MockDatabase();
const mockLogger = { info: jest.fn(), error: jest.fn() };
const testService = new UserService(mockDb, mockLogger);
3.2 シンプルなDIコンテナの自作
📝 注意: 以下の実装は概念説明用のシンプルな例です。依存関係の解決に
__paramTypesという静的プロパティを手動で定義しています。実際のプロダクションではreflect-metadataを使ったデコレーターベースのコンテナ(tsyringe、inversifyなど)を使用することを推奨します。
// ✅ タイプセーフなDIコンテナの実装(概念説明用)
type Constructor<T = any> = new (...args: any[]) => T;
interface Registration {
factory: Constructor | any;
singleton: boolean;
}
class Container {
private services: Map<string, Registration> = new Map();
private singletons: Map<string, any> = new Map();
register<T>(token: string, implementation: Constructor<T>): void {
this.services.set(token, { factory: implementation, singleton: true });
}
registerValue<T>(token: string, value: T): void {
this.services.set(token, { factory: value, singleton: true });
}
resolve<T>(token: string): T {
const registration = this.services.get(token);
if (!registration) throw new Error(`Service ${token} not found`);
if (typeof registration.factory !== "function") {
return registration.factory;
}
if (this.singletons.has(token)) {
return this.singletons.get(token);
}
// __paramTypesは手動で定義したトークンリスト(概念説明用)
const paramTypes = (registration.factory as any).__paramTypes || [];
const dependencies = paramTypes.map((param: string) => this.resolve(param));
const instance = new registration.factory(...dependencies);
this.singletons.set(token, instance);
return instance;
}
}
// 使用例
class AppLogger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class AppDatabase {
constructor(private config: { host: string }) {}
async query(sql: string) {
console.log(`Querying ${this.config.host}: ${sql}`);
}
}
class UserService {
// 依存関係のトークンを手動で宣言(概念説明用)
static __paramTypes = ["database", "logger"];
constructor(
private db: AppDatabase,
private logger: AppLogger
) {}
async getUser(id: string) {
this.logger.log(`Getting user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = $1`);
}
}
const container = new Container();
container.registerValue("dbConfig", { host: "localhost" });
container.register("database", AppDatabase);
container.register("logger", AppLogger);
container.register("userService", UserService);
const userService = container.resolve<UserService>("userService");
3.3 tsyringe(Microsoft製の軽量DI)
// ✅ tsyringeの使用例
// tsconfig.json に以下を追加:
// {
// "experimentalDecorators": true,
// "emitDecoratorMetadata": true
// }
import "reflect-metadata";
import { container, injectable, inject, singleton } from "tsyringe";
interface Logger {
log(message: string): void;
}
const LoggerToken = "Logger";
@injectable()
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
@injectable()
class Database {
constructor(@inject("DbConfig") private config: { host: string }) {}
async query(sql: string): Promise<any> {
console.log(`Querying ${this.config.host}: ${sql}`);
}
}
@singleton()
class UserService {
constructor(
@inject("Database") private db: Database,
@inject(LoggerToken) private logger: Logger
) {}
async getUser(id: string): Promise<any> {
this.logger.log(`Fetching user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = $1`);
}
}
// 依存関係の登録
container.register("DbConfig", { useValue: { host: "localhost" } });
container.register("Database", { useClass: Database });
container.register(LoggerToken, { useClass: ConsoleLogger });
const userService = container.resolve(UserService);
3.4 awilix(デコレーター不要の軽量DI)
// ✅ awilixの使用例(Node.js向け)
import { createContainer, asClass, asValue } from "awilix";
class Logger {
info(message: string) {
console.log(`[INFO] ${message}`);
}
}
class Database {
private config: { host: string };
constructor({ config }: { config: { host: string } }) {
this.config = config;
}
async query(sql: string) {
console.log(`Querying ${this.config.host}: ${sql}`);
}
}
class UserService {
private db: Database;
private logger: Logger;
constructor({ db, logger }: { db: Database; logger: Logger }) {
this.db = db;
this.logger = logger;
}
async getUser(id: string) {
this.logger.info(`Fetching user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = $1`);
}
}
const container = createContainer();
container.register({
config: asValue({ host: "localhost" }),
logger: asClass(Logger).singleton(),
db: asClass(Database).singleton(),
userService: asClass(UserService).singleton()
});
const userService = container.resolve<UserService>("userService");
3.5 Inversify(エンタープライズ向け機能満載)
// ✅ inversifyの使用例
import "reflect-metadata";
import { Container, injectable, inject } from "inversify";
const TYPES = {
Logger: Symbol.for("Logger"),
Database: Symbol.for("Database"),
UserService: Symbol.for("UserService"),
DbConfig: Symbol.for("DbConfig")
};
interface Logger {
log(message: string): void;
}
@injectable()
class FileLogger implements Logger {
log(message: string): void {
console.log(`[FILE] ${message}`);
}
}
@injectable()
class Database {
constructor(
@inject(TYPES.DbConfig) private config: { host: string }
) {}
async query(sql: string): Promise<any> {
console.log(`Querying ${this.config.host}: ${sql}`);
}
}
@injectable()
class UserService {
constructor(
@inject(TYPES.Database) private db: Database,
@inject(TYPES.Logger) private logger: Logger
) {}
async getUser(id: string): Promise<any> {
this.logger.log(`Fetching user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = $1`);
}
}
const container = new Container();
container.bind(TYPES.DbConfig).toConstantValue({ host: "localhost" });
container.bind(TYPES.Logger).to(FileLogger).inSingletonScope();
container.bind(TYPES.Database).to(Database).inSingletonScope();
container.bind(TYPES.UserService).to(UserService);
const userService = container.get<UserService>(TYPES.UserService);
4. DIコンテナの比較 – ライブラリ選定ガイド
比較表
| 機能 | awilix | inversify | tsyringe | typedi |
|---|---|---|---|---|
| デコレーター不要 | ✅ | ❌ | ❌ | ❌ |
| reflect-metadata | ❌ 不要 | ✅ 必須 | ✅ 必須 | ✅ 必須 |
| 登録スタイル | 関数型 | バインディング | クラスベース | グローバル |
| スコープ管理 | ✅ 優れている | ✅ 複雑 | ⚠️ 制限あり | ⚠️ 制限あり |
| バンドルサイズ | 小 | 大 | 小 | 中 |
| Node.js向け | ⭐ 最適 | ✅ 良い | ✅ 良い | ✅ 良い |
| フロントエンド向け | ⭐ 最適 | ⚠️ 重い | ✅ 良い | ✅ 良い |
| 学習コスト | 低 | 高 | 中 | 低 |
選定フローチャート
プロジェクトの種類は?
│
├── Node.jsバックエンド(リクエストスコープが必要)
│ │
│ └── awilix ⭐
│
├── フロントエンド(React/Vue)+ 軽量さ重視
│ │
│ ├── デコレーターを使いたい → tsyringe
│ └── デコレーターを使いたくない → awilix
│
├── 大規模エンタープライズ(複雑な依存関係)
│ │
│ └── inversify
│
└── 小さなプロジェクト/プロトタイプ
│
└── 手動DI or awilix
各ライブラリの特徴
awilix
- デコレーター不要、シンプルなセットアップ
- リクエストスコープのサポートが強力
- Node.jsバックエンドに最適
- 明示的な登録でデバッグが容易
tsyringe
- Microsoft製、アクティブにメンテナンス
- デコレーターベースだがセットアップは比較的簡単
- モダンなTypeScriptプロジェクトに適している
- シングルトン/トランジエントのシンプルな管理
inversify
- 最も機能が豊富(コンテキストバインディング、ミドルウェアなど)
- エンタープライズ向けの複雑なユースケースに対応
- 学習曲線が急で、設定も多い
- 大きなプロジェクトで真価を発揮
5. 課題 – シニア向けのチャレンジ問題
課題1: 手動DIコンテナの拡張
以下の要件を満たすDIコンテナを実装してください。
- シングルトンスコープとトランジエントスコープの両方をサポート
- 循環依存を検出してエラーにする
- ファクトリーパターンで動的な依存関係解決をサポート
💡 ヒント:
resolve呼び出し中に「解決中トークン」を追跡することで循環依存を検出できます。
✅ 解答を見る(クリック)
interface Registration {
factory: (container: Container) => any;
singleton: boolean;
}
class Container {
private registrations: Map<string, Registration> = new Map();
private instances: Map<string, any> = new Map();
private resolving: Set<string> = new Set();
register<T>(
token: string,
factory: (container: Container) => T,
options?: { singleton?: boolean }
): void {
this.registrations.set(token, {
factory,
singleton: options?.singleton ?? false
});
}
resolve<T>(token: string): T {
const registration = this.registrations.get(token);
if (!registration) throw new Error(`Token "${token}" not found`);
// 循環依存の検出
if (this.resolving.has(token)) {
throw new Error(`Circular dependency detected for "${token}"`);
}
if (registration.singleton && this.instances.has(token)) {
return this.instances.get(token);
}
this.resolving.add(token);
const instance = registration.factory(this);
this.resolving.delete(token);
if (registration.singleton) {
this.instances.set(token, instance);
}
return instance;
}
}
課題2: テスト容易性の比較
以下のコードをリファクタリングして、テスタブルにしてください。
class PaymentProcessor {
private apiKey = process.env.STRIPE_API_KEY;
private stripe = new Stripe(this.apiKey);
async processPayment(amount: number, cardToken: string) {
console.log(`Processing ${amount}`);
return await this.stripe.charges.create({ amount, source: cardToken });
}
}
💡 ヒント:
PaymentGatewayインターフェースを切り出し、コンストラクタインジェクションで差し替え可能にします。
✅ 解答を見る(クリック)
interface PaymentGateway {
charge(amount: number, source: string): Promise<any>;
}
class StripeGateway implements PaymentGateway {
constructor(private apiKey: string) {}
async charge(amount: number, source: string) {
const stripe = new Stripe(this.apiKey);
return stripe.charges.create({ amount, source });
}
}
class PaymentProcessor {
constructor(private gateway: PaymentGateway) {}
async processPayment(amount: number, cardToken: string) {
console.log(`Processing ${amount}`);
return this.gateway.charge(amount, cardToken);
}
}
// テストではモックを注入
class MockGateway implements PaymentGateway {
async charge(amount: number, source: string) {
return { id: "mock_charge", amount };
}
}
// テスト
const processor = new PaymentProcessor(new MockGateway());
// 実際のStripe APIを呼ばずにテストできる
課題3: プロジェクトに合ったDIライブラリの選定
以下のプロジェクト要件に最適なDIライブラリを選び、理由を説明してください。
- プロジェクトA: Express.js製のREST API。リクエストごとにDB接続を分けたい。チームはデコレーターに否定的。
- プロジェクトB: React + TypeScriptのフロントエンド。状態管理にDIを導入したい。バンドルサイズを小さく保ちたい。
- プロジェクトC: 複数のマイクロサービスを持つ大規模エンタープライズ。複雑な依存関係と動的なバインディングが必要。
💡 ヒント: 比較表の「リクエストスコープ」「バンドルサイズ」「デコレーター不要」の列を参考にしてください。
✅ 解答を見る(クリック)
プロジェクトA → awilix
- デコレーター不要でチームの要望に合う
- リクエストスコープのサポートが強力
- Node.jsバックエンドで実績豊富
プロジェクトB → tsyringe
- 軽量でバンドルサイズが小さい
- Reactとの相性が良い
- Microsoft製で安定性が高い
プロジェクトC → inversify
- 機能が最も豊富
- コンテキストバインディングやミドルウェアをサポート
- 大規模な依存関係グラフを管理可能
6. まとめ
今日学んだこと
| 概念 | 説明 |
|---|---|
| Dependency Injection | 依存関係を外部から注入するデザインパターン |
| Inversion of Control | 制御の流れを逆転させ、結合度を下げる原則 |
| DIコンテナ | 依存関係の登録と解決を自動化するライブラリ |
| コンストラクタインジェクション | 最も推奨される注入方法 |
| シングルトンスコープ | アプリケーション全体で1つのインスタンスを共有 |
| トランジエントスコープ | 解決のたびに新しいインスタンスを作成 |
| リクエストスコープ | HTTPリクエストごとにインスタンスを分離 |
シニアへのアドバイス
DIを適切に使うと、コードのテスト容易性、保守性、拡張性が劇的に向上します。ただし、DIコンテナを導入することが目的ではありません。まずはコンストラクタインジェクションから始め、プロジェクトの規模に応じてコンテナの導入を検討しましょう。
また、デコレーターベースのDIは強力ですが、設定の複雑さとトレードオフになります。チームの経験値とプロジェクトの要件を考慮して選定してください。
Have a nice day! 🚀
