Dependency Inversion Principle

Dependency Inversion Principle (DIP) — принцип SOLID, гласящий: модули высокого уровня не должны зависеть от модулей низкого уровня; оба должны зависеть от абстракций (интерфейсов).

Зачем нужно

Без DIP бизнес-логика напрямую зависит от деталей реализации: UserService импортирует MySQLUserRepository. Смена MySQL на PostgreSQL требует правки в UserService. С DIP UserService зависит от абстракции (интерфейса IUserRepository), а конкретная реализация подставляется снаружи (Dependency Injection). Это делает код тестируемым, расширяемым и слабосвязанным.

Где используется

  • Смена реализации без изменения бизнес-логики: MySQL → PostgreSQL, HTTP → GraphQL
  • Тестирование: подставляем mock-репозитории вместо реальных БД
  • Плагинные архитектуры: разные реализации одного интерфейса
  • NestJS, Angular: встроенный IoC-контейнер основан на DIP
  • Clean Architecture: Use Cases зависят от интерфейсов Repository, не от реализаций

Основной контент

Нарушение DIP (тесная связность)

// ПЛОХО: бизнес-логика зависит от конкретной реализации
class MySQLUserRepository {
  async findById(id) {
    return mysql.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

class UserService {
  constructor {
    // Жёсткая зависимость — нельзя заменить без изменения UserService
    this.repository = new MySQLUserRepository();
  }

  async getUser(id) {
    return this.repository.findById(id);
  }
}

// Тест требует реальной БД — плохо!

Соблюдение DIP (через Dependency Injection)

// Абстракция — «контракт» (в JS: соглашение или duck typing)
// IUserRepository: { findById(id), save(user), findByEmail(email) }

// Реализация 1: MySQL
class MySQLUserRepository {
  async findById(id) { return mysql.query('SELECT * ...', [id]); }
  async save(user) { return mysql.query('INSERT ...', [user]); }
}

// Реализация 2: In-Memory (для тестов)
class InMemoryUserRepository {
  constructor { this.users = new Map(); }
  async findById(id) { return this.users.get(id) || null; }
  async save(user) { this.users.set(user.id, user); return user; }
}

// Высокоуровневый модуль зависит от АБСТРАКЦИИ, не реализации
class UserService {
  constructor(userRepository) { // зависимость инжектируется извне
    this.repository = userRepository;
  }

  async getUser(id) {
    const user = await this.repository.findById(id);
    if (!user) throw new Error(`Пользователь ${id} не найден`);
    return user;
  }

  async createUser(data) {
    const user = { id: crypto.randomUUID, ...data, createdAt: new Date };
    return this.repository.save(user);
  }
}

// Production: реальная БД
const productionService = new UserService(new MySQLUserRepository);

// Тест: in-memory, без БД
const testRepo = new InMemoryUserRepository();
const testService = new UserService(testRepo);
await testService.createUser({ name: 'Иван', email: 'ivan@test.com' });
// Тест быстрый, изолированный, детерминированный

Простой IoC-контейнер

// IoC-контейнер управляет созданием и инжекцией зависимостей
class Container {
  constructor {
    this.bindings = new Map();
  }

  bind(key, factory) {
    this.bindings.set(key, factory);
    return this;
  }

  make(key) {
    const factory = this.bindings.get(key);
    if (!factory) throw new Error(`Не зарегистрировано: ${key}`);
    return factory(this);
  }
}

// Конфигурация
const container = new Container();

container.bind('userRepository', () => new MySQLUserRepository);
container.bind('userService', (c) => new UserService(c.make('userRepository')));

// Для тестов переопределяем только нужную зависимость
const testContainer = new Container();
testContainer.bind('userRepository', () => new InMemoryUserRepository);
testContainer.bind('userService', (c) => new UserService(c.make('userRepository')));

const service = container.make('userService');

Частые ошибки

  • Инжекция конкретного класса: constructor(repo: MySQLRepository) — по-прежнему зависимость от реализации; нужна абстракция constructor(repo: IUserRepository).
  • Service Locator вместо DI: const repo = container.get('userRepo') внутри класса — класс сам ищет зависимость, что скрывает зависимости и затрудняет тестирование. DI лучше.
  • Избыточный DIP для простых модулей: утилитные функции (форматирование, вычисления) не нуждаются в абстракциях. DIP применяется к внешним зависимостям и инфраструктурному коду.

Связанные темы

Ресурсы