---
title: "IoC в обычном JavaScript (ES6+)"
description: "Практическое объяснение инверсии управления в JavaScript с ES-модулями, контрактами и внедрением зависимостей."
date: 2023-07-17
---

Инверсию управления (IoC) в JavaScript бывает сложнее почувствовать, чем
в PHP, Java или TypeScript: в языке нет встроенных интерфейсов. Здесь
рассматривается ES6+ код, разложенный на ES-модули и связанный через
`import`/`export`.

## Импорт как зависимость

На уровне модуля любой статический или динамический импорт — зависимость
от другого модуля. Например, у `service` есть зависимость от конкретного
`logger`:

``` js
import logger from './logger.js';
export class Service { /* ... */ }
```

Разработчик сервиса открывает импорт и видит реализацию:

``` js
export default {
  error: (msg) => console.error(msg),
  info: (msg) => console.info(msg),
};
```

Затем он использует её прямо внутри модуля:

``` js
import logger from './logger.js';
export class Service {
  exec() { logger.info('Service is running.'); }
}
```

Так исходники оказываются жёстко собраны `import`-ами в единую иерархию
ещё при написании кода.

## Инверсия управления

Представим, что логгеров несколько: консольный, файловый, сетевой, для
базы данных. Разные разработчики могут сделать их независимо, но сначала
им нужно договориться о контракте:

``` js
/** @interface */
class ILogger {
  error(msg) {}
  info(msg) {}
}
```

Это не исполняемый код, а соглашение, которое JSDoc делает понятным
человеку и IDE. Реализации следуют ему:

``` js
class LoggerFile { error(msg) {} info(msg) {} }
class LoggerNet  { error(msg) {} info(msg) {} }
```

Автор `Service` не знает, какой логгер будет выбран при запуске, поэтому
принимает зависимость извне — классически через конструктор:

``` js
export class Service {
  constructor(logger) { this.logger = logger; }
  exec() { this.logger.info('Service is running.'); }
}
```

Это и есть IoC: источник зависимости выбирает не сервис, а внешний для
него код — обычно объектный контейнер. Связи задаются во время
выполнения и могут зависеть от условий, а не закрепляются во всех
модулях статическими импортами.

## Что даёт такой подход

В проекте с DI почти все импорты могут остаться в composition root —
месте, где приложение собирают. Сами модули должны разрешать внедрение
нужных зависимостей через конструктор или setter. Тогда они слабее
связаны и переносятся между проектами, если новая среда предоставляет
нужные контракты.

Особенно наглядно это в тесте. Вместо файлового или сетевого логгера
передаём простую заглушку:

``` js
const logger = { error(msg) {}, info(msg) {} };
const service = new Service(logger);
service.exec();
```

Тест управляет поведением зависимости, не ссылаясь на конкретную
реализацию. В архитектурном смысле IoC сначала отвечает за то, как
изготовлены отдельные «кирпичи», и лишь затем — как они сложены в
здание. Хорошо подготовленные модули можно соединять по-разному.

<zoom-img src="/medium/img/1b2e701f331d/image-01.jpg" alt="Кирпичи, из которых строится приложение" width="100%"></zoom-img>

Поэтому ES6-модули, не привязанные статическими импортами к конкретным
службам, могут быть строительными блоками и браузерного, и
Node.js-приложения.
