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