Using Factory Functions for Dependency Injection
Factory functions create service instances with their dependencies, enabling testability and flexibility.
Basic Factory Pattern
class AudioService {
constructor(
private sampleRate: number,
private channels: number
) {}
calculateDuration(buffer: Buffer): number {
// ... implementation
}
}
// Factory function
export function createAudioService(
sampleRate: number = 24000,
channels: number = 1
): AudioService {
return new AudioService(sampleRate, channels);
}
With Multiple Dependencies
class AssetGenerationService {
constructor(
private geminiClient: IGeminiClient,
private assetManager: IAssetManager,
private articleManager: IArticleManager,
private audioService: IAudioService
) {}
async generateAssets(article: FullArticle): Promise<void> {
// ... implementation
}
}
// Factory function with all dependencies
export function createAssetGenerationService(
geminiClient: IGeminiClient,
assetManager: IAssetManager,
articleManager: IArticleManager,
audioService: IAudioService
): AssetGenerationService {
return new AssetGenerationService(
geminiClient,
assetManager,
articleManager,
audioService
);
}
Default Dependencies
import { createGeminiClient } from '../ai/gemini-client';
import { createAssetManager } from '../storage/asset-manager';
import { createArticleManager } from '../storage/article-manager';
import { createAudioService } from './audio-service';
export function createAssetGenerationService(
geminiClient?: IGeminiClient,
assetManager?: IAssetManager,
articleManager?: IArticleManager,
audioService?: IAudioService
): AssetGenerationService {
return new AssetGenerationService(
geminiClient ?? createGeminiClient(),
assetManager ?? createAssetManager(),
articleManager ?? createArticleManager(),
audioService ?? createAudioService()
);
}
Configuration Object
interface ServiceConfig {
apiTimeout: number;
maxRetries: number;
enableCache: boolean;
}
class ApiClient {
constructor(private config: ServiceConfig) {}
}
export function createApiClient(config: Partial<ServiceConfig> = {}): ApiClient {
const defaultConfig: ServiceConfig = {
apiTimeout: 30000,
maxRetries: 3,
enableCache: true,
};
return new ApiClient({ ...defaultConfig, ...config });
}
For Testing
// Production
const service = createAssetGenerationService();
// Testing
const mockGemini = new MockGeminiClient();
const mockAssets = new MockAssetManager();
const service = createAssetGenerationService(
mockGemini,
mockAssets,
mockArticleManager,
mockAudioService
);
// Verify calls
await service.generateAssets(article);
expect(mockGemini.generateImageCalls.length).toBe(7);
Singleton Pattern
let instance: YouTubeClient | null = null;
export function createYouTubeClient(authManager: AuthManager): YouTubeClient {
if (!instance) {
instance = new YouTubeClient(authManager);
}
return instance;
}
// Or with closure
export const createYouTubeClient = (() => {
let instance: YouTubeClient | null = null;
return (authManager: AuthManager) => {
if (!instance) {
instance = new YouTubeClient(authManager);
}
return instance;
};
})();
Async Factory
class DatabaseService {
constructor(private connection: Connection) {}
static async create(connectionString: string): Promise<DatabaseService> {
const connection = await connect(connectionString);
return new DatabaseService(connection);
}
}
// Factory function
export async function createDatabaseService(
connectionString: string
): Promise<DatabaseService> {
return await DatabaseService.create(connectionString);
}
Factory with Builder Pattern
class Service {
private constructor(
private dependency1: Dep1,
private dependency2: Dep2,
private config: Config
) {}
static builder() {
return new ServiceBuilder();
}
}
class ServiceBuilder {
private dep1?: Dep1;
private dep2?: Dep2;
private config: Config = defaultConfig();
withDependency1(dep: Dep1): this {
this.dep1 = dep;
return this;
}
withDependency2(dep: Dep2): this {
this.dep2 = dep;
return this;
}
withConfig(config: Partial<Config>): this {
this.config = { ...this.config, ...config };
return this;
}
build(): Service {
if (!this.dep1 || !this.dep2) {
throw new Error('Missing dependencies');
}
return new Service(this.dep1, this.dep2, this.config);
}
}
// Usage
const service = Service.builder()
.withDependency1(dep1)
.withDependency2(dep2)
.withConfig({ timeout: 5000 })
.build();
Benefits
| Aspect | Direct Construction | Factory Function |
|---|---|---|
| Testability | Difficult to mock | Easy dependency injection |
| Configuration | Hardcoded defaults | Flexible parameters |
| Singletons | Manual management | Built-in support |
| Dependency hiding | Hidden dependencies | Explicit dependencies |
| Validation | In constructor | In factory |
Best Practices
- Always export factory functions from service modules
- Use interfaces for dependencies (enforces contracts)
- Provide sensible defaults for optional dependencies
- Document dependencies clearly in factory signature
- Keep factories simple - complex construction in separate builders
- Use factories for testability - inject mocks easily
- Consistent naming -
create{ServiceName}()