Внедрение зависимостей (Dependency Injection) в TypeScript: Полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в Dependency Injection в TypeScript

Dependency Injection (DI) — это один из ключевых паттернов проектирования, который позволяет создавать слабосвязанные, тестируемые и легко поддерживаемые приложения. В контексте TypeScript, благодаря строгой типизации и возможностям декораторов, реализация DI становится особенно элегантной и безопасной.

В этой статье мы рассмотрим, что такое Dependency Injection, как его реализовать на TypeScript, и какие инструменты могут упростить этот процесс. Вы узнаете, как DI помогает бороться с жесткими связями между классами и делает ваш код более гибким.

Что такое Dependency Injection и зачем он нужен?

Dependency Injection (внедрение зависимостей) — это паттерн, при котором объект получает свои зависимости извне, а не создает их самостоятельно. Это реализует принцип Inversion of Control (IoC) — инверсии управления.

Проблема без DI:

class Logger {
    log(message: string): void {
        console.log(message);
    }
}

class UserService {
    private logger: Logger;

    constructor() {
        this.logger = new Logger(); // Жесткая связь
    }

    createUser(name: string): void {
        this.logger.log(`User ${name} created`);
    }
}

В этом примере UserService жестко привязан к конкретной реализации Logger. Если мы захотим заменить логгер на, скажем, файловый или удаленный, нам придется изменять код UserService.

Решение с DI:

interface ILogger {
    log(message: string): void;
}

class ConsoleLogger implements ILogger {
    log(message: string): void {
        console.log(message);
    }
}

class FileLogger implements ILogger {
    log(message: string): void {
        // Запись в файл
    }
}

class UserService {
    constructor(private logger: ILogger) {} // Зависимость внедряется

    createUser(name: string): void {
        this.logger.log(`User ${name} created`);
    }
}

// Использование
const service = new UserService(new ConsoleLogger());

Преимущества DI:

  • Тестируемость — легко подменить реальные зависимости на mock-объекты.
  • Гибкость — можно менять поведение системы без изменения кода классов.
  • Разделение ответственности — классы не занимаются созданием своих зависимостей.
  • Переиспользование — зависимости можно использовать в разных частях приложения.

Реализация Dependency Injection в TypeScript вручную

Самый простой способ — внедрять зависимости через конструктор или свойства. TypeScript с его интерфейсами и модификаторами доступа делает это очень удобно.

Внедрение через конструктор (Constructor Injection)

interface IRepository<T> {
    getAll(): Promise<T[]>;
    getById(id: number): Promise<T | null>;
}

class UserRepository implements IRepository<User> {
    async getAll(): Promise<User[]> {
        // Реальная логика работы с БД
        return [];
    }

    async getById(id: number): Promise<User | null> {
        return null;
    }
}

class UserController {
    constructor(
        private repository: IRepository<User>,
        private logger: ILogger
    ) {}

    async getUsers(): Promise<User[]> {
        this.logger.log('Fetching users...');
        return this.repository.getAll();
    }
}

// Создание графа зависимостей
const logger = new ConsoleLogger();
const repository = new UserRepository();
const controller = new UserController(repository, logger);

Внедрение через свойства (Property Injection)

class EmailService {
    private sender!: ISender;

    setSender(sender: ISender): void {
        this.sender = sender;
    }

    sendEmail(to: string, message: string): void {
        this.sender.send(to, message);
    }
}

// Использование
const emailService = new EmailService();
emailService.setSender(new SmtpSender());

Хотя ручное внедрение работает, в больших проектах оно становится громоздким. Здесь на помощь приходят DI-контейнеры.

Использование DI-контейнера: InversifyJS

InversifyJS — это мощный IoC-контейнер для TypeScript, который использует декораторы для автоматического разрешения зависимостей.

Установка

npm install inversify reflect-metadata --save

Не забудьте включить в tsconfig.json:

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

Пример использования

import { Container, injectable, inject } from 'inversify';
import 'reflect-metadata';

// Определяем типы для идентификации
const TYPES = {
    Logger: Symbol.for('Logger'),
    UserService: Symbol.for('UserService'),
    Database: Symbol.for('Database')
};

// Регистрируем классы как injectable
@injectable()
class Logger {
    log(msg: string): void {
        console.log(msg);
    }
}

@injectable()
class Database {
    constructor(@inject(TYPES.Logger) private logger: Logger) {}

    connect(): void {
        this.logger.log('Connected to database');
    }
}

@injectable()
class UserService {
    constructor(
        @inject(TYPES.Logger) private logger: Logger,
        @inject(TYPES.Database) private db: Database
    ) {}

    createUser(name: string): void {
        this.logger.log(`Creating user: ${name}`);
        this.db.connect();
    }
}

// Настройка контейнера
const container = new Container();
container.bind<Logger>(TYPES.Logger).to(Logger);
container.bind<Database>(TYPES.Database).to(Database);
container.bind<UserService>(TYPES.UserService).to(UserService);

// Получаем экземпляр
const userService = container.get<UserService>(TYPES.UserService);
userService.createUser('Alice');

Преимущества InversifyJS

  • Автоматическое разрешение — контейнер сам строит граф зависимостей.
  • Управление жизненным циклом — можно задать singleton, transient или request scope.
  • Чистая архитектура — код не загромождается ручным созданием объектов.
  • Поддержка асинхронных фабрик — для сложных сценариев инициализации.

Продвинутые техники и лучшие практики

1. Использование абстракций

Всегда программируйте на уровне интерфейсов, а не конкретных реализаций. Это позволяет легко подменять зависимости.

interface IPaymentGateway {
    processPayment(amount: number): Promise<boolean>;
}

@injectable()
class StripeGateway implements IP                

Блоги

Рекомендаций по книгам