@teqfw/di: Coding JavaScript like a Java boss

от автора

Эта статья для тех, кто, как и я, хочет программировать на 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.

Спасибо тем, кто читал, и всем приятного кодинга!! 🧂🥃🍋‍🟩


ссылка на оригинал статьи https://habr.com/ru/articles/901678/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *