@teqfw/di: Coding JavaScript like a Java boss
- пятница, 18 апреля 2025 г. в 00:00:06
Эта статья для тех, кто, как и я, хочет программировать на JavaScript в Java-стиле. Для тех, кто находит вдохновение в балансе между строгой архитектурной дисциплиной Java и творческой свободой JavaScript. Ранее я уже публиковал "философию" своей платформы TeqFW
, а также инструкции для LLM (раз, два) по оформлению es-модулей в приложениях, написанных в стиле TeqFW
. На этот раз я делюсь инструкцией для LLM по использованию внедрения зависимостей в таких приложениях.
Для тех, кто не совсем понимает, что значит "программировать на JavaScript в Java-стиле", приведу рабочий пример — это Node.js-утилита @flancer64/smtp-logger. Она сохраняет в базу данных все email'ы, которые Postfix
отправляет наружу. Мне как раз понадобился такой функционал — и я реализовал его в стиле TeqFW
: с явным управлением зависимостями и строгой модульной структурой.
Под катом - пример JS-кода в Java-стиле.
Сама утилита небольшая (~300 строк JS-кода в 11 файлах), основная работа делается в пакетах:
{
"dependencies": {
"@teqfw/di": "^0.32.0",
"better-sqlite3": "^11.9.1",
"dotenv": "^16.5.0",
"knex": "^3.1.0",
"mailparser": "^3.7.2",
"minimist": "^1.2.8",
"pg": "^8.14.1"
}
}
@teqfw/di
: позднее связывание с использованием внедрения зависимостей в конструкторе (обеспечивает Java-like coding).
dotenv
: загрузка переменные окружения.
mailparser
: разбор MIME-сообщений.
minimist
: парсинг аргументов командной строки.
knex
: DBAL для различных СУБД, я использую SQLite (better-sqlite3
) для тестов и PostgreSQL (pg
) на проде.
Несмотря на такое количество зависимых npm-пакетов в коде утилиты статический импорт встречается ровно 1 раз - в ./index.js
. Он небольшой, поэтому привожу код полностью:
#!/usr/bin/env node
'use strict';
import Container from '@teqfw/di';
const container = new Container();
const resolver = container.getResolver();
resolver.addNamespaceRoot('Smtp_Log_', import.meta.resolve('./src'));
/** @type {Smtp_Log_App} */
const app = await container.get('Smtp_Log_App$');
app.run().catch(console.error);
Видно, что вся логика сосредоточена в объекте класса Smtp_Log_App
. Ниже приведён его конструктор — чтобы было понятно, как именно зависимости (runtime-объекты) попадают в app-объект.
export default class Smtp_Log_App {
constructor(
{
Smtp_Log_App_Configurator$: configurator,
Smtp_Log_Cmd_Init$: cmdInit,
Smtp_Log_Cmd_Log$: cmdLog,
Smtp_Log_Enum_Command$: CMD,
}
) {
this.run = async function () {};
}
}
Те, у кого есть практический опыт программирования на Java, без труда заметят влияние этого языка на представленный код.
А вот пример того, как внедряются зависимости из сторонних npm-пакетов:
export default class Smtp_Log_Cmd_Log {
constructor(
{
'node:mailparser': mailparser,
// ...
}
) {
const {simpleParser} = mailparser;
// ...
}
}
Этот код аналогичен классическому:
import {simpleParser} from 'mailparser';
Классический подход со статическим импортом похож на сварку — он намертво соединяет компоненты, лишая систему гибкости. Внедрение npm-пакета через конструктор больше напоминает сборку из деталей LEGO: элементы могут различаться по форме, но объединяются через одинаковый интерфейс, что позволяет свободно и разнообразно их комбинировать.
Такой подход особенно ценен в тестировании и при расширении функциональности. Вот так можно замокировать весь пакет mailparser
при тестировании Smtp_Log_Cmd_Log
:
const mock = {
simpleParser: async () => 'parsed data',
};
const cmd = new Smtp_Log_Cmd_Log({'node:mailparser': mock});
Отказ от статических импортов и переход к связыванию объектов во время выполнения превращает все точки соединения компонентов из "сварных" в "разборные". За сборку всех элементов в единое приложение отвечает Object Container (import Container from '@teqfw/di'
) — именно он играет роль главного координатора, того самого "босса".
А теперь — немного инструкций для LLM, которые более подробно раскрывают основы применения внедрения зависимостей в рамках платформы TeqFW.
## Свод правил DI для TeqFW (v1.0)
- Код делится на:
- **Данные**: enum, DTO, фабрики.
- **Логика**: обработчики, координаторы.
- Один модуль = одна роль.
- Все зависимости передаются как **единый объект `deps`**, деструктурируемый сразу и плоско.
- Внутри модулей `import` запрещён.
- Допустимые экспорты:
- `default`-объект (например, enum),
- фабричная функция,
- класс с `constructor({deps})`.
- Идентификаторы зависимостей (ключи в `deps`):
- `Ns_Component$`: default-экспорт, синглтон.
- `Ns_Component$$`: default-экспорт, новый экземпляр.
- `Ns_Component.name$`: именованный экспорт, синглтон.
- `node:pkg`, `node:pkg.name`: внешние пакеты, как есть.
- Контейнер резолвит: `Ns_` → путь; `A_B_C` → `A/B/C.js`.
- Контейнер использует `import()` для загрузки; модули сами `import` не используют.
- Разрешён только `import` контейнера:
```js
import Container from '@teqfw/di';
```
- Зависимости должны быть:
- Явными,
- Названы по соглашению,
- Не сокращены (до `:` — имя для контейнера).
- Синглтоны кэшируются контейнером.
- Модули должны быть валидными ESM через `export`, даже без `import`.
- Весь код должен соответствовать этим правилам для совместимости с TeqFW.
## Правила внедрения зависимостей (DI) в TeqFW (v1.0)
Этот документ формализует правила внедрения зависимостей (Dependency Injection, DI) в архитектуре TeqFW.
Документ предназначен для обработки языковыми моделями (LLM) и использования при генерации кода.
Материал структурирован в виде принципов и примеров, иллюстрирующих допустимые формы кода. Особое внимание уделяется
совместимости с DI-контейнером TeqFW, предсказуемости адресации зависимостей, модульной изоляции и отказу от прямого
импорта.
---
### 📌 Принцип 1. Функциональное разделение кода
> Код делится на две независимые группы:
>
> - **Данные (структуры)** — описывают *что это за данные* (enum, DTO, фабрики, значения по умолчанию).
> - **Обработчики** — описывают *что делать с данными* (логика, команды, координация).
📌 Один модуль — одна роль.
Нельзя совмещать структуру и поведение в одном модуле.
#### ✅ Пример: enum-структура
```js
// ✅ ES-модуль, без import, подходит для DI
const Smtp_Log_Enum_Command = {
INIT_DB: 'init-db',
LOG: 'log',
};
export default Smtp_Log_Enum_Command;
```
#### ✅ Пример: DTO + фабрика
```js
// ✅ Поддерживает DI через параметры конструктора
export default class Smtp_Log_Dto_Config {
constructor({Smtp_Log_Enum_Command$: CMD}) {
this.create = function (data) {
const res = Object.assign(new Dto(), data);
res.command = data?.command ?? CMD.LOG;
return res;
};
}
}
class Dto {
command;
dbName;
dbPass;
dbUser;
}
```
#### ✅ Пример: обработчик
```js
// ✅ Обработчик с внедрением всех зависимостей через deps
export default class Smtp_Log_App {
constructor({
Smtp_Log_App_Configurator$: configurator,
Smtp_Log_Cmd_Init$: cmdInit,
Smtp_Log_Cmd_Log$: cmdLog,
Smtp_Log_Enum_Command$: CMD,
}) {
this.run = async function (opts) {
// use dependencies
};
}
}
```
---
### 📌 Принцип 2. Совместимость экспортов с DI-контейнером
> Каждый модуль должен экспортировать сущность, пригодную для создания через DI-контейнер и повторного использования.
Допустимые варианты экспортов:
- **Объект** — используется напрямую (например, `enum`).
- **Фабричная функция** — получает зависимости через параметры, возвращает объект.
- **Класс** — получает зависимости через конструктор.
📌 Все зависимости передаются только через параметры — `import` запрещён.
📌 DI-контейнер сам создаёт зависимости и передаёт их в модуль.
---
### 📌 Принцип 3. Жизненный цикл: один модуль — один экземпляр
> Каждый модуль создаётся один раз и работает как изолированный экземпляр.
#### ✅ Пример: класс
```js
export default class Smtp_Log_App {
constructor({configurator}) {
this.run = async function (opts) {
const cfg = configurator.build();
// ...
};
}
}
```
#### ✅ Пример: фабрика
```js
export default function createSmtpLogApp({configurator}) {
return {
async run(opts) {
const cfg = configurator.build();
// ...
}
};
}
```
📌 Все экземпляры создаются контейнером один раз.
📌 Такие модули можно использовать вручную, передавая зависимости явно.
📌 Если модуль используется как синглтон (`$`), контейнер кэширует экземпляр после первого создания.
---
### 📌 Принцип 4. Зависимости передаются через `deps`
> Все зависимости передаются в виде **одного объекта `deps`**, который деструктурируется при создании модуля.
📌 Деструктуризация должна быть:
- **сразу при входе** (в конструкторе или фабрике);
- **плоской** — без вложенных структур;
- **явной** — без `import`.
#### ✅ Пример
```js
export default class Some_Module {
constructor({logger, config}) {
// Используем logger и config
}
}
```
---
### 📌 Принцип 5. Именование и адресация зависимостей
> Все зависимости в объекте `deps` обозначаются по соглашению `Namespace_Component$`.
#### 📌 Форматы:
- `Namespace_Component$` — default-экспорт, singleton
- `Namespace_Component$$` — default-экспорт, новый экземпляр
- `Namespace_Component.name$` — именованный экспорт, singleton
- `node:pkg` — весь npm-модуль, as-is
- `node:pkg.export` — именованный экспорт из npm-модуля, as-is
#### ✅ Пример внедрения
```js
export default class Smtp_Log_App {
constructor(
{
Smtp_Log_App_Configurator$: configurator,
Smtp_Log_Cmd_Init$: cmdInit,
Smtp_Log_Cmd_Log$: cmdLog,
}
) {
// ...
}
}
```
📌 Все зависимости видимы DI-контейнеру по имени свойства (до `:`), а в коде работают через локальные переменные (после
`:`).
#### ⚠️ Важно для моделей:
- Не сокращай имена (`Configurator$` — недостаточно).
- Не модифицируй имена до `:` — они важны для контейнера.
- Не используй `import` — даже для одной зависимости.
- Всегда используй деструктуризацию `deps`.
---
### 📌 Принцип 6. Резолвинг зависимостей через пространства имён
> Контейнер разрешает зависимости по идентификатору `Namespace_Component$`, сопоставляя
`Namespace_` с путём в файловой системе.
#### ✅ Пример настройки контейнера
```js
#!/usr/bin/env node
'use strict';
import Container from '@teqfw/di';
const container = new Container();
const resolver = container.getResolver();
resolver.addNamespaceRoot('Smtp_Log_', import.meta.resolve('./src'));
const app = await container.get('Smtp_Log_App$');
app.run().catch(console.error);
```
#### 📌 Как работает резолвинг
- `Smtp_Log_` → `./src/`
- `Dto_Config` → `Dto/Config.js`
📌 `A_B_C` → `A/B/C.js`
#### 📁 Примеры:
| Идентификатор | Пространство | Компонент | Путь к файлу |
|------------------------|--------------|--------------|-----------------------|
| `Smtp_Log_App$` | `Smtp_Log_` | `App` | `./src/App.js` |
| `Smtp_Log_Dto_Config$` | `Smtp_Log_` | `Dto_Config` | `./src/Dto/Config.js` |
| `Ns_Group_Web_App$` | `Ns_Group_` | `Web_App` | `./group/Web/App.js` |
---
### 📌 Принцип 7. Соответствие стандартам ESM
> Модули TeqFW не используют `import`, но остаются валидными ES-модулями благодаря использованию `export`.
#### ⚠️ Разрешён только `import` при инициализации контейнера:
```js
import Container from '@teqfw/di';
```
📌 Все прочие зависимости внедряются через `deps`. 📌 Контейнер использует динамическую загрузку (`import()`), чтобы найти
и создать зависимости.
Это позволяет избегать жёстких связей и исключает необходимость использования `import` в прикладных модулях. 📌 Это
архитектурное ограничение, а не технический запрет. Оно обеспечивает изоляцию и предсказуемость.
---
### ✅ Итог
Все приведённые принципы направлены на унификацию структуры модулей, автоматизируемость создания и анализа кода, а также
минимизацию неявных зависимостей. Документ формирует строгое техническое основание для корректной работы DI-контейнера и
предсказуемого поведения при генерации кода с использованием LLM.
Отклонения от описанных правил приводят к потере совместимости с архитектурой TeqFW.
Все свои эмбеддинг-инструкции я выложил в TeqFW Help Desk на основе кастомного GPT-чата. Это, конечно же, не полноценный support, зато 24х7.
Спасибо тем, кто читал, и всем приятного кодинга!! 🧂🥃🍋🟩