Что такое внедрение зависимостей?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Dependency Injection (DI) — это паттерн проектирования, при котором объект получает свои зависимости извне, вместо того чтобы создавать их самостоятельно. Это один из ключевых принципов чистой архитектуры и SOLID (D — Dependency Inversion).
Проблема без DI
// Плохо: класс создаёт свои зависимости
class UserService {
constructor() {
this.database = new PostgresDatabase(); // Жёсткая связь
this.emailService = new GmailService(); // Жёсткая связь
}
async register(email, password) {
this.database.save(email, password);
this.emailService.send(email, 'Welcome!');
}
}
// Проблемы:
// 1. Невозможно протестировать (нельзя подменить database)
// 2. Сложно менять реализацию (нужно менять класс)
// 3. Если DatabaseService изменится, нужна переписка UserService
Решение: Dependency Injection
// Хорошо: зависимости передаются в constructor
class UserService {
constructor(database, emailService) {
this.database = database;
this.emailService = emailService;
}
async register(email, password) {
this.database.save(email, password);
this.emailService.send(email, 'Welcome!');
}
}
// Использование
const db = new PostgresDatabase();
const email = new GmailService();
const userService = new UserService(db, email);
await userService.register('user@example.com', 'password123');
Три типа DI
1. Constructor Injection (конструктор)
class UserService {
constructor(database) {
this.database = database;
}
async getUser(id) {
return this.database.findById(id);
}
}
const db = new Database();
const service = new UserService(db);
Плюсы:
- Все зависимости видны в конструкторе
- Объект полностью инициализирован при создании
- Легко тестировать
Минусы:
- Конструктор может стать очень большим
2. Setter Injection
class UserService {
setDatabase(database) {
this.database = database;
}
async getUser(id) {
return this.database.findById(id);
}
}
const service = new UserService();
service.setDatabase(new Database());
Минусы:
- Объект может быть не полностью инициализирован
- Зависимости не очевидны
3. Interface Injection
interface Injectable {
inject(dependencies: object): void;
}
class UserService implements Injectable {
inject(dependencies) {
this.database = dependencies.database;
this.emailService = dependencies.emailService;
}
}
IoC Container (Inversion of Control)
Проблема: если DI повсеместно, получим спагетти-код из создания объектов.
Решение: использовать IoC контейнер, который управляет жизненным циклом объектов.
// Простой IoC контейнер
class Container {
constructor() {
this.services = new Map();
}
register(name, factory) {
this.services.set(name, factory);
}
resolve(name) {
const factory = this.services.get(name);
if (!factory) throw new Error(`Service ${name} not found`);
return factory(this);
}
}
// Использование
const container = new Container();
container.register('database', () => new PostgresDatabase());
container.register('emailService', () => new GmailService());
container.register('userService', (c) => {
return new UserService(
c.resolve('database'),
c.resolve('emailService')
);
});
const userService = container.resolve('userService');
Express + DI пример
class Database {
async query(sql) {
// реальный запрос
}
}
class UserRepository {
constructor(database) {
this.database = database;
}
async findById(id) {
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
class UserService {
constructor(repository) {
this.repository = repository;
}
async getUser(id) {
return this.repository.findById(id);
}
}
class UserController {
constructor(userService) {
this.userService = userService;
}
async handleGetUser(req, res) {
const user = await this.userService.getUser(req.params.id);
res.json(user);
}
}
// IoC контейнер
const container = new Container();
container.register('database', () => new Database());
container.register('userRepository', (c) => new UserRepository(c.resolve('database')));
container.register('userService', (c) => new UserService(c.resolve('userRepository')));
container.register('userController', (c) => new UserController(c.resolve('userService')));
// Express
app.get('/users/:id', (req, res) => {
const controller = container.resolve('userController');
controller.handleGetUser(req, res);
});
Тестирование с DI
// Без DI: невозможно тестировать
class UserService {
constructor() {
this.database = new RealDatabase(); // Жёсткая связь
}
}
// С DI: легко подменить
class MockDatabase {
async findById(id) {
return { id, name: 'John' };
}
}
test('UserService.getUser', async () => {
const mockDb = new MockDatabase();
const userService = new UserService(mockDb);
const user = await userService.getUser(1);
expect(user.name).toBe('John');
});
Frameworks с встроенным DI
NestJS (рекомендуется для Node.js)
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
constructor(private database: Database) {}
async getUser(id) {
return this.database.findById(id);
}
}
NestJS автоматически управляет DI через декораторы.
Awilix (лёгкий IoC контейнер)
const { createContainer, asClass, asValue } = require('awilix');
const container = createContainer();
container.register({
database: asClass(Database).singleton(),
userService: asClass(UserService).singleton(),
userController: asClass(UserController).singleton(),
});
const userController = container.resolve('userController');
SOLID принципы DI
D — Dependency Inversion Principle:
// Плохо: зависит от конкретной реализации
class UserService {
constructor() {
this.payment = new StripePayment(); // Жёсткая связь
}
}
// Хорошо: зависит от интерфейса
class UserService {
constructor(paymentProvider) { // Может быть Stripe, PayPal, etc.
this.payment = paymentProvider;
}
}
Когда использовать DI
- Всегда в production коде
- Для тестируемости
- Для гибкости архитектуры
- Для масштабируемости
Когда не использовать
- Простые скрипты
- Прототипы
- Код с одной вспомогательной библиотекой
Dependency Injection — это мощный паттерн, который делает код чище, тестируемее и более гибким. В современном Node.js разработка без DI немыслима для production приложений.