TeqFW: Внедрение зависимостей

Дата публикации: 2021-06-09

В основе любой JS-программы лежит создание объектов и связывание их между собой. В IT уже давно, больше 20 лет, используются техники создания и связывания объектов, самой популярной из которых стало внедрение зависимостей (Dependency Injection) — JSR-330, PSR-11, …

Для того, чтобы платформа TeqFW могла создавать и связывать объекты, должны выполняться следующие условия:

DI-контейнер в TeqFW реализован в пакете @teqfw/di и может использоваться отдельно от платформы.

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

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

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):

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-модуля (вручную, без использования контейнера):

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-контейнер должен вернуть вызывающей стороне:

constructor(spec) {
const dep = spec[‘depId’];
}

await container.get(‘dep1’); ### Именованные и импортируемые

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

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 все идентификаторы делятся на две большие группы:

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

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

В @teqfw/di для загрузки ES-модуля используется динамический импорт, результатом которого является специальный объект Module (см. пункт “Модули” в “Javascript: исходный код и его отображение при отладке”).

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

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

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

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

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

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

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

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

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

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

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 это делается так:

constructor(spec) {
const named = spec[‘namedSingleton’];
const inst = spec[‘EsModId#name$$’];
const single = spec[‘EsModId$’];
} Особенностью @teqfw/di является то, что контейнер прерывает процесс создания запрошенного объекта, если обнаруживает неизвестную зависимость, подгружает исходники зависимости и создает зависимость, после чего вновь пытается создать запрошенный объект. Таким образом, первые строки конструктора запрошенного объекта могут выполняться несколько раз, если в процессе приходилось несколько раз прерывать процесс и подгружать нужные исходники.

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

constructor({named, EsModId, EsModId$}) {}

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

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

container.addSourceMapping(‘EsModId’, ‘./relative/path’);
container.addSourceMapping(‘EsModId’, ‘/absolute/path’, true); Первый способ применяется, если контейнер используется в браузере, второй — в nodejs-приложениях.

В карте сопоставления прописывается корневой каталог с исходниками, дальнейшее сопоставление идентификаторов исходникам идет через использование namespace’ов, где разделителем имен каталогов является _:

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

Резюме

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

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

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