Factory Pattern with Dependency Injection

Creating objects with explicit dependencies for testability.

Problem

Classes with implicit dependencies (global state, direct imports) are hard to test and modify. We need a way to make dependencies explicit and swappable.

Solution

Use factory functions that accept dependencies as parameters:

// Types for dependencies
interface Logger {
  log(message: string): void;
  error(message: string): void;
}
 
interface Storage {
  get(key: string): string | null;
  set(key: string, value: string): void;
}
 
// The service that needs dependencies
interface ChatEngine {
  process(input: string): string;
  getHistory(): string[];
}
 
// Factory function with explicit dependencies
function createChatEngine(deps: { logger: Logger; storage: Storage; rules: Rule[] }): ChatEngine {
  const { logger, storage, rules } = deps;
  const history: string[] = [];
 
  return {
    process(input: string): string {
      logger.log(`Processing: ${input}`);
      history.push(input);
 
      for (const rule of rules) {
        if (rule.matches(input)) {
          const response = rule.apply(input);
          history.push(response);
          return response;
        }
      }
 
      return "I don't understand.";
    },
 
    getHistory(): string[] {
      return [...history];
    },
  };
}

Usage

Production

import { consoleLogger } from './logger';
import { localStorageAdapter } from './storage';
import { defaultRules } from './rules';
 
const engine = createChatEngine({
  logger: consoleLogger,
  storage: localStorageAdapter,
  rules: defaultRules,
});

Testing

import { describe, it, expect, vi } from 'vitest';
 
describe('ChatEngine', () => {
  it('logs input when processing', () => {
    const mockLogger = {
      log: vi.fn(),
      error: vi.fn(),
    };
 
    const engine = createChatEngine({
      logger: mockLogger,
      storage: { get: vi.fn(), set: vi.fn() },
      rules: [],
    });
 
    engine.process('hello');
 
    expect(mockLogger.log).toHaveBeenCalledWith('Processing: hello');
  });
});

Variations

Partial Dependencies with Defaults

interface EngineOptions {
  logger?: Logger;
  timeout?: number;
}
 
function createEngine(options: EngineOptions = {}) {
  const logger = options.logger ?? console;
  const timeout = options.timeout ?? 5000;
  // ...
}

Class-Based Alternative

class ChatEngine {
  constructor(
    private logger: Logger,
    private storage: Storage,
    private rules: Rule[]
  ) {}
 
  process(input: string): string {
    this.logger.log(`Processing: ${input}`);
    // ...
  }
}

Trade-offs

When to Use

  • Services with multiple dependencies
  • Code that needs unit testing
  • Components that vary by environment

When NOT to Use

  • Simple utility functions
  • Pure functions with no side effects
  • One-off scripts

Source

Extracted from Eliza - Pattern matcher with injectable rules and state.