---
title: "TeqFW: Внедрение зависимостей"
description: "В основе любой JS-программы лежит создание объектов и связывание их между собой. В IT уже [давно](http://mvpjava.com/brief-history-dependency-injection/), больше 20 лет, используют"
date: 2021-06-09
---

В основе любой JS-программы лежит создание объектов и связывание их
между собой. В IT уже
[давно](http://mvpjava.com/brief-history-dependency-injection/), больше
20 лет, используются техники создания и связывания объектов, самой
популярной из которых стало внедрение зависимостей (Dependency
Injection) — [JSR-330](http://javax-inject.github.io/javax-inject/),
[PSR-11](https://www.php-fig.org/psr/psr-11/), …

Для того, чтобы платформа
[TeqFW](https://wiredgeese.com/%D1%87%D1%82%D0%BE-%D1%82%D0%B0%D0%BA%D0%BE%D0%B5-teqfw-208d5938205)
могла создавать и связывать объекты, должны выполняться следующие
условия:

- код должен оформляться в виде ES-модулей с расширением `*.mjs`;
- конструктор класса или фабричная функция должны иметь один входной
  параметр — `spec` (specification);
- контейнер объектов должен иметь возможность сопоставить идентификатор
  объекта в спецификации с именем файла, содержащего ES-модуль с кодом
  конструктора или фабричной функции этого объекта, либо объект,
  соответствующий идентификатору, должен быть загружен в контейнер
  вручную;

DI-контейнер в TeqFW реализован в пакете [<span class="citation"
cites="teqfw/di">@teqfw/di</span>](https://github.com/teqfw/di) и может
использоваться отдельно от платформы.

## Основы работы DI-контейнеров

Типовое обращение к контейнеру объектов выглядит примерно так:

```js
const obj = container.get(id);
```

Последовательность действий контейнера:

1.  Определить по `id` , что за объект хочет получить вызывающая
    сторона.
2.  Проверить внутреннее хранилище на предмет наличия в нём
    запрашиваемого объекта, если объект есть в хранилище — вернуть его.
3.  Если же объект нужно создавать, то по `id` определить, где находится
    файл с исходным кодом объекта и загрузить исходники.
4.  Разобрать спецификацию входных зависимостей конструктора объекта
    (набор идентификаторов).
5.  Найти во внутреннем хранилище зависимости согласно спецификации или
    создать заново.
6.  Создать запрашиваемый объект с использованием собранных
    зависимостей.
7.  Сохранить созданный объект во внутреннее хранилище для последующего
    использования (при необходимости).
8.  Вернуть созданный объект вызывающей стороне.

Последовательность действий рекурсивная — повторяется в пункте 5 для
каждой зависимости, до тех пор, пока всё дерево зависимостей
запрашиваемого объекта не будет создано.

## ES-модуль

Прежде всего нужно понимать, что может экспортировать ES-модуль — т.е.,
что именно может использовать DI-контейнер для создания объектов после
загрузки исходного кода ES-модуля.

Вот типовой ES-модуль, который мог бы использоваться в `@teqfw/di`
(`es6.mjs`):

```js
const obj = {name: 'Simple Object'};
class Clazz {
  constructor(spec) {
    this.name = 'instance from class constructor';
  }
}
function Factory(spec) {
  return {name: 'instance from factory'};
}
export {
  obj as ObjTmpl,
  Clazz as default,
  Factory,
};
```

А это пример того, как вызывающая сторона могла бы создавать объекты при
помощи кода из этого ES-модуля (вручную, без использования контейнера):

```js
import Def from './es6.mjs';
import {Factory} from './es6.mjs';
import {ObjTmpl} from './es6.mjs';
const spec = {}; // empty specification
const instClass = new Def(spec);
const instFact = Factory(spec);
const instTmpl = Object.assign(ObjTmpl, {});
```

Итого, DI-контейнер, после загрузки ES-модуля, может создавать новые
объекты на основе экспорта модуля:

- используя классы;
- используя фабричные функции;
- используя объекты в качестве шаблона;

## Идентификаторы зависимостей

Идентификатор зависимости — это строка, идентифицирующая объект, который
ожидает получить конструктор объекта (фабричная функция) в качестве
зависимости, или который DI-контейнер должен вернуть вызывающей стороне:

```js
class Consumer {
constructor(spec) {
  const dep = spec['depId'];
}
}
```

…

```js
await container.get('dep1');
```

### Именованные и импортируемые

В самом простом случае разработчик может создавать объекты вручную и
помещать их прямо в контейнер под произвольными идентификаторами:

```js
import Container from '@teqfw/di';
const container = new Container();
container.set('dep1', {name: 'first'});
container.set('dep2', {name: 'second'});
const obj = await container.get('dep1');
```

Но в большинстве случаев нас интересует способ автоматического
нахождения контейнером ES-модуля, подгрузка исходников и определение
способа создания зависимости (класс, фабричная функция или
шаблон-объект).

В `@teqfw/di` все идентификаторы делятся на две большие группы:

- *именованные зависимости*: название начинается со строчной буквы
  (`connection`, `conf`, `i18n`, …);
- *импортируемые зависимости*: названия начинаются с прописной буквы
  (`EsModuleId`);

Именованные зависимости добавляются в контейнер вручную через
`container.set(id, obj)`, для импортируемых зависимостей есть правила
сопоставления идентификаторов путям к исходникам (рассмотрим позднее).

### Модуль и экспорт модуля

В `@teqfw/di` для загрузки ES-модуля используется [динамический
импорт](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports),
результатом которого является специальный объект Module (см. пункт
“Модули” в “[Javascript: исходный код и его отображение при
отладке](https://habr.com/ru/post/524526/)”).

Необходимо различать, хотим ли мы использовать в качестве зависимости
модуль целиком или какой-то определённый экспорт из данного модуля. В
`@teqfw/di` для этого используется знак `#`:

- `EsModuleId`: идентификатор для модуля целиком;
- `EsModuleId#ExportName`: идентификатор для экспорта `ExportName` в
  модуле `EsModuleId`;
- `EsModuleId#default` и `EsModuleId#`: оба идентификатора равнозначны и
  указывают на экспорт `default` в модуле `EsModuleId`;

### Функция и результат функции

Зависимости объекта в `@teqfw/di` передаются в спецификации в
конструктор или фабричную функцию:

```js
class Clazz {
  constructor(spec) {}
}
function Factory(spec) {}
```

Как различить случай, когда мы хотим получить от контейнера сам класс (функцию), а когда — экземпляр объекта данного класса (результат работы функции)? В идентификаторе зависимости для @teqfw/di это отражается при помощи символа $:

- `EsModuleId#ExportName`: получить объект (класс, функцию) с именем
  `ExportName` из модуля `EsModuleId`.
- `EsModuleId#ExportName$`: получить объект, созданный при помощи
  конструктора (фабричной функции), являющегося экспортом `ExportName`
  модуля `EsModuleId`.

Для создания объекта из `default`-экспорта модуля эти идентификаторы
равнозначны:

- `EsModuleId#default$`
- `EsModuleId#$`
- `EsModuleId$`

### Singleton и новый экземпляр

Иногда контейнер должен использовать один и тот же экземпляр в качестве
зависимости для всех (или группы) объектов приложения, а иногда каждый
раз должен создаваться новый экземпляр объекта. В идентификаторах
зависимости это отражается через удвоение символа *доллар* -`$$`:

- `EsModuleId$` и `EsModuleId#ExportName$`: объект создается один раз,
  при первом запросе, и сохраняется в контейнере, для всех последующих
  запросов используется ранее сохраненный объект.
- `EsModuleId$$` и `EsModuleId#ExportName$$`: каждый раз создается новый
  экземпляр объекта.

При этом неважно, какой из объектов первым запросил создание singleton’а
— все остальные получат этот же экземпляр. В некотором смысле любой
DI-контейнер является global-объектом. Кто-то может сказать, что это
антипаттерн, и отказаться от использования DI — его полное право.

### Сводная таблица идентификаторов

Итого в `@teqfw/di` используются следующие идентификаторы зависимостей:

```js
let id1 = 'named'; // named singleton been added manually
let id2 = 'EsModId'; // ES module
let id3 = 'EsModId#'; // default export of ES module
let id4 = 'EsModId#name'; // named export of ES module
let id5 = 'EsModId$'; // singleton from default export
let id6 = 'EsModId$$'; // new instance from default export
let id7 = 'EsModId#name$'; // singleton from named export
let id8 = 'EsModId#name$$'; // new instance from named export
```

## Декларация зависимостей

Вся мощь контейнера раскрывается тогда, когда мы описываем зависимости,
необходимые для создания объекта, в конструкторе (фабричной функции). В
`@teqfw/di` это делается так:

```js
class Consumer {
constructor(spec) {
  const named = spec['namedSingleton'];
  const inst = spec['EsModId#name$$'];
  const single = spec['EsModId$'];
}
}
```

Особенностью @teqfw/di является то, что контейнер прерывает процесс создания запрошенного объекта, если обнаруживает неизвестную зависимость, подгружает исходники зависимости и создает зависимость, после чего вновь пытается создать запрошенный объект. Таким образом, первые строки конструктора запрошенного объекта могут выполняться несколько раз, если в процессе приходилось несколько раз прерывать процесс и подгружать нужные исходники.

В простых случаях можно использовать такую форму декларации
зависимостей, которая не дает возможности вставить код в конструктор до
разрешения всех нужных зависимостей:

```js
class Consumer {
constructor({named, EsModId, EsModId$}) {}
}
```

## Загрузка исходников

Чтобы контейнер по идентификатору зависимости мог обнаружить файл с
исходным кодом соответствующего ES-модуля, нужна карта сопоставления
идентификаторов зависимостей файлам с исходниками. В `@teqfw/di`
добавлением позиций в карту делается так:

```js
container.addSourceMapping('EsModId', './relative/path');
container.addSourceMapping('EsModId', '/absolute/path', true);
```

Первый способ применяется, если контейнер используется в браузере, второй — в nodejs-приложениях.

В карте сопоставления прописывается корневой каталог с исходниками,
дальнейшее сопоставление идентификаторов исходникам идет через
использование
[namespace’ов](https://wiredgeese.com/%D0%BD%D0%B0%D0%B8%D0%BC%D0%B5%D0%BD%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2-%D0%BA%D0%BE%D0%B4%D0%B0-%D0%B2-js-fbcabbee5931),
где разделителем имен каталогов является `_`:

```text
EsModId_PathTo_Mod => /absolute/path/PathTo/Mod.mjs
```

## Резюме

DI-контейнер [<span class="citation"
cites="teqfw/di">@teqfw/di</span>](https://github.com/teqfw/di)
позволяет использовать в качестве зависимостей как сами ES-модули, так и
отдельные элементы из экспорта ES-модулей, а также создавать новые
экземпляры объектов или использовать один единственный объект для всего
приложения. Причем один и тот же код может использоваться как в
браузерах, так и в `nodejs`-приложениях.

Типовой код для ES-модуля, используемого в `@teqfw/di`:

```js
export default class Mod {
  constructor(spec) {
    const Clazz = spec['Lib_Dep#'];
    const single = spec['Lib_Dep$'];
    const inst = spec['Lib_Dep$$'];
    // …
  }
}
```
