依存性注入(DI)設計¶
概要¶
ぷよぷよゲームでは、Clean Architectureの原則に従い、依存関係逆転の原則(DIP)を実現するため、カスタム依存性注入(DI)システムを実装しています。
アーキテクチャ¶
主要コンポーネント¶
1. Container クラス¶
ファイル: src/infrastructure/di/Container.ts
DIコンテナの核となるクラスで、サービスの登録と解決を管理します。
機能: - サービス登録(通常・シングルトン・ファクトリ) - 型安全なサービス解決 - サービス存在確認 - コンテナクリア
実装詳細:
export class Container {
private services: Map<string | symbol, unknown> = new Map()
private factories: Map<string | symbol, () => unknown> = new Map()
private singletons: Map<string | symbol, unknown> = new Map()
// シングルトン → ファクトリ → 通常サービスの優先順位で解決
resolve<T>(token: symbol): T
registerSingleton<T>(token: string | symbol, instance: T): void
registerFactory<T>(token: string | symbol, factory: () => T): void
register<T>(token: string | symbol, instance: T): void
}
2. サービストークン¶
ファイル: src/infrastructure/di/tokens.ts
型安全な依存解決のためのSymbolベーストークンを定義します。
// Symbol.forを使用してグローバル一意性を確保
export const GAME_USE_CASE = Symbol.for('GameUseCase')
export const SOUND_EFFECT_SERVICE = Symbol.for('SoundEffectService')
export const BACKGROUND_MUSIC_SERVICE = Symbol.for('BackgroundMusicService')
export const HIGH_SCORE_SERVICE = Symbol.for('HighScoreService')
export const GAME_SETTINGS_SERVICE = Symbol.for('GameSettingsService')
3. 型定義¶
ファイル: src/infrastructure/di/types.ts
DIで管理するサービスのインターフェース定義を提供します。
export interface SoundEffectService {
play(soundType: string): void
}
export interface BackgroundMusicService {
play(musicType: string): void
fadeOut(duration: number): Promise<void>
stop(): void
}
export interface HighScoreService {
getHighScores(): Array<{ score: number; date: string; rank: number }>
isHighScore(score: number): boolean
addScore(score: number): Array<{ score: number; date: string; rank: number }>
}
export interface GameSettingsService {
getSetting(key: 'autoDropSpeed'): number
getSetting(key: 'showShadow'): boolean
getSetting(key: string): unknown
}
4. サービスセットアップ¶
ファイル: src/infrastructure/di/setup.ts
DIコンテナへのサービス登録を管理します。
export function setupContainer(): void {
// アプリケーション層のサービス(ファクトリパターン)
container.registerFactory(GAME_USE_CASE, () => new GameUseCase())
// サービス層のサービス(シングルトンパターン)
container.registerSingleton(SOUND_EFFECT_SERVICE, soundEffect)
container.registerSingleton(BACKGROUND_MUSIC_SERVICE, backgroundMusic)
container.registerSingleton(HIGH_SCORE_SERVICE, highScoreService)
container.registerSingleton(GAME_SETTINGS_SERVICE, gameSettingsService)
}
使用方法¶
1. アプリケーション初期化¶
import { initializeApplication } from './infrastructure/di'
function App() {
// DIコンテナの初期化(アプリケーション起動時に1回実行)
useState(() => {
initializeApplication()
return null
})
// サービス解決
const [gameUseCase] = useState(() =>
container.resolve<GameUseCase>(GAME_USE_CASE)
)
}
2. サービス解決¶
// 型安全なサービス解決
const soundEffectService = container.resolve<SoundEffectService>(SOUND_EFFECT_SERVICE)
const backgroundMusicService = container.resolve<BackgroundMusicService>(BACKGROUND_MUSIC_SERVICE)
const highScoreService = container.resolve<HighScoreService>(HIGH_SCORE_SERVICE)
const gameSettingsService = container.resolve<GameSettingsService>(GAME_SETTINGS_SERVICE)
設計パターン¶
1. サービスロケーターパターン¶
DIコンテナはサービスロケーターとして機能し、必要なサービスをクライアントに提供します。
2. シングルトンパターン¶
音響サービス、設定サービスなど、アプリケーション全体で単一のインスタンスを共有するサービスに適用。
3. ファクトリパターン¶
GameUseCaseなど、使用時に新しいインスタンスが必要なサービスに適用。
型安全性¶
1. Symbolベーストークン¶
文字列ベースのトークンではなくSymbolを使用することで、タイプミスや名前衝突を防止。
2. TypeScript Generics¶
サービス解決時にGenericsを使用し、コンパイル時の型チェックを実現。
3. インターフェース契約¶
各サービスを具象クラスではなくインターフェースで定義し、実装の詳細を隠蔽。
テスト支援¶
1. モックサービス注入¶
テスト時は本物のサービスの代わりにモックサービスを注入可能:
// テスト用のモックサービス登録
container.registerSingleton(SOUND_EFFECT_SERVICE, mockSoundEffectService)
2. 分離テスト¶
DIによりサービス間の依存関係が切り離されるため、各サービスを独立してテスト可能。
利点¶
- 疎結合: コンポーネント間の直接的な依存関係を排除
- テスタビリティ: モックサービスの注入により単体テストが容易
- 拡張性: 新しいサービスの追加が既存コードに影響しない
- 保守性: インターフェースベースの設計により実装変更の影響を局所化
- 型安全性: TypeScriptの型システムと組み合わせたコンパイル時チェック
制約事項¶
- 学習コスト: DI概念の理解が必要
- 初期設定: コンテナとサービス登録の初期設定が必要
- 実行時解決: サービス解決は実行時に行われるため、設定ミスは実行時エラーとなる
今後の拡張可能性¶
- 自動サービス発見: アノテーションベースの自動サービス登録
- ライフサイクル管理: サービスのスコープとライフサイクル管理の強化
- 設定ベース注入: 外部設定ファイルによるサービス構成
- 非同期サービス: Promiseベースの非同期サービス解決