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 применяется к внешним зависимостям и инфраструктурному коду.