Pull to refresh
55
Alex Gusev@flancer

Я кодирую, потому что я кодирую…

0,1
Rating
99
Subscribers
Send message

Так бывает и без DI. Вероятно, вы проверяете то, что не нужно, а то, что нужно, не проверяете. Делайте наоборот.

Что именно вас смущает в этом коде?

export default function Service({ repository }) {
  return {
    async getProfile(id) {
      const name = await repository.findNameById(id);
      return { id, name };
    },
  };
}

Фабрика создаёт объект и инжектит в него зависимость. Обычный JS-код. Можете переписать его под классы, если вам больше нравятся классы.

А фабрика, в свою очередь, является реализацией DI на языке, в сочетании с замыканиями. И без всяких фреймворков.

Можно и так - вручную. Фреймворки лишь помогают унифицировать общие подходы в проекте. Или в проектах. Или в отдельной экосистеме. Всё зависит от масштаба.

Про "удержание стоимости верификации под контролем" было вообще больно читать.

Под стоимостью верификации я имею в виду рост числа возможных композиций. Если у компонента несколько вариативных зависимостей, пространство сборок образуется как декартово произведение их реализаций и растёт мультипликативно.

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

"Программирование - безнадежно проигранная борьба с непреодолимой сложностью кода и вероломством требований" (с) Jonathan Edwards

Согласен, что фраза "удержание чего бы то ни было под контролем" в программировании звучит излишне оптимистично. DI помогает проиграть эту борьбу чуточку позже.

"Ручной менеджмент токенов" - это лишь один из возможных вариантов управления зависимостями. Я, например, и мои агенты, пишем так (ну, или примерно так):

export default function Service({ repository }) {
  return {
    async getProfile(id) {
      const name = await repository.findNameById(id);
      return { id, name };
    },
  };
}

export const __deps__ = {
  repository: "App_User_Repository$",
};

При отсутствующей runtime-рефлексии в JS приходится "колхозить" runtime-рефлексию самому. Зависимости всех доступных снаружи экспортов отдельного es6-модуля прописываются в специальном экспорте __deps__. После этого анализ зависимостей модуля можно автоматизировать полностью. Синтаксис "языка токенов" может быть любым, я взял для себя namespace'ы из PHP Zend1 фреймворка. Главное, чтобы этот "язык" упаковывал в токене:

  • адрес npm-пакета и es6-модуля в нём;

  • имя экспорта в es6-модуле;

  • форму существования зависимости (singleton or transient).

После этого достаточно в настройке контейнера в Composition Root привязать физическое расположение пакета (в локальной файловой системе или в вебе) к соответствующему namespace'у (App_ или App_User_) и разрешение зависимостей из __deps__ происходит автоматически, как в C#, Java, PHP, ...

Решение с __deps__, кстати, мне ИИ подсказал. Это логика "силиконовых", а не "кожаных". Им так норм.

Сделал при помощи codex-агента скилл на основе этой публикации - architecture-foundations. Можно попробовать применить с ИИ-агентом (claude, codex, opencode,...). Установка через npx :

npx skills add flancer32/skill-architecture-foundations --skill architecture-foundations

или вручную, через копирование каталога architecture-foundations в ~/.agents/skills/.

Интересно попробовать описанные в публикации оси для анализа своих проектов.

Сложность: Средний

Спасибо, посмеялся. Я уже на одной четверти текста перестал связывать одно с другим, а для тиктокера это вообще непролазный лес! Но в целом - я в восторге!! Никогда не смотрел на разработку программ в таком ключе. Получилось очень интересно и познавательно (ну, там, где я понял о чём речь, разумеется :))) ).

Попросил ИИшечку привести пост к моему когнитивному уровню - вроде стало чуть полегче. Выкладываю выжимку, может кому сгодится. Там, правда, под меня "заточено", но каждый может перегнать страницу в PDF, скормить "своей" ИИшке и получить трансляцию идей ТС'а "под себя".

Вот "под меня"

Интересная статья, но, на мой взгляд, её ценность не в том, что программирование буквально «доказывает» Платона, Аристотеля или Лейбница. Философские параллели здесь скорее язык, позволяющий разложить архитектурные решения по независимым вопросам.

В упрощённом виде автор предлагает спрашивать о системе следующее:

  1. Где находится общее: в явно объявленном типе, в фактической структуре объекта или только в именах и соглашениях?

  2. Что первично: устойчивый объект с изменяемым состоянием или поток событий, из которого состояние каждый раз выводится?

  3. Кто может менять состояние: любой обладатель ссылки или только сама сущность через обработку сообщений?

  4. Сохраняется ли форма в runtime: может ли система во время исполнения распознать тип, схему и смысл полученного сообщения?

  5. Кто владеет исполнением: внешний поток, единый event loop, coroutine, actor или отдельный процесс?

  6. Где находится цель: только у пользователя либо представлена внутри системы и самостоятельно ею преследуется?

Из этого получается несколько вполне практических выводов.

Первый: главная проблема конкурентности — сочетание нескольких исполнителей с общим изменяемым состоянием. Лечить её можно в двух независимых направлениях: убрать изменяемость или убрать совместное владение. Отсюда два устойчивых подхода: свободно разделяемые immutable-значения и изолированное mutable-состояние с единственным владельцем.

Второй: у каждого изменяемого состояния должен быть понятный хозяин. Остальные компоненты не должны менять его напрямую — они предъявляют команду или сообщение, а владелец сам решает, как и когда его обработать. Это снижает число возможных комбинаций взаимодействий, хотя оплачивается очередями, сериализацией, задержками и необходимостью согласования.

Третий: интерфейс полезен именно как узкий контракт, а не как способ передавать потомкам кусок готового «бытия». Чем больше состояния и поведения спускается через наследование, тем сильнее связанность и тем сложнее независимо изменять части системы. Поэтому композиция и чистые интерфейсы обычно устойчивее глубоких иерархий классов.

Четвёртый: event sourcing не отменяет объекты, а переносит источник истины. В обычном ООП истина находится в текущем состоянии объекта. В event-sourced-системе — в журнале, а объект становится проекцией истории. Это полезно там, где важны аудит, восстановление, повторная обработка и объяснимость, но не является универсально лучшей моделью.

Пятый: runtime-системе необязательно иметь рефицированные типы самого языка, но ей необходимо какое-то доступное представление протокола — schema, discriminator, валидатор, таблица диспетчеризации или сгенерированный декодер. Замкнутый компонент может самостоятельно интерпретировать сообщение только тогда, когда формат сообщения представлен во время исполнения.

Шестой: автономность исполнения и наличие цели — разные свойства. Actor может иметь собственный цикл обработки и ни к чему не стремиться. Оптимизатор может преследовать цель, оставаясь полностью управляемым внешней петлёй. Для агентных систем это различие принципиально: отдельно должны проектироваться движитель, цель, полномочия и возможность её коррекции.

Наиболее сильной мне кажется последняя часть статьи про LLM-агентов. В обычной системе сообщение и код разделены: данные не должны переписывать поведение обработчика. У языкового агента и инструкции, и данные, и описание цели представлены одним субстратом — текстом. Поэтому message passing сам по себе не обеспечивает изоляции: сообщение может изменить не состояние, а интерпретацию правил и цели.

Практическое следствие — на внутренних границах агентных систем свободный естественный язык, вероятно, должен уступать место более жёстким протоколам:

  • явно заданному типу сообщения;

  • разделению данных, инструкций и системных правил;

  • валидируемому payload;

  • ограниченным capabilities;

  • аудируемому журналу действий;

  • отдельной процедуре изменения цели и полномочий.

Для мультиагентных систем из этого следуют два относительно устойчивых варианта. Либо один большой агент с внутренним разделением ролей, единым контекстом и общей целью. Либо несколько изолированных агентов с явными ролями, полномочиями, аудитом, арбитражем и процедурами согласования. Толпа агентов, свободно общающихся на естественном языке при размытых целях и широких правах, выглядит действительно неустойчивой.

При этом я бы осторожнее относился к сильным тезисам статьи. Аналогия между actor и монадой или между Paxos и предустановленной гармонией красива, но сама по себе ничего не доказывает. Заявленная ортогональность осей тоже не абсолютна: в реальных языках и runtime эти свойства часто сцеплены. А «закон сложности» работает только тогда, когда систему действительно можно разрезать по узким границам. Плохо декомпозированная распределённая система легко оказывается сложнее хорошего модульного монолита.

Поэтому я бы сформулировал практическое ядро статьи так:

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

Для агентных систем к этому добавляется ещё одно требование: изолировать нужно не только память, но также интерпретацию, инструкции, полномочия и цель. Здесь обычной инкапсуляции уже недостаточно — появляются роли, политики, аудит, арбитраж и ответственность.

В любом случае, есть над чем подумать. Спасибо за статью! (y)

Какую проблему вы решаете с помощью DI?

Лично я решаю с помощью DI проблему "распределённой разработки кода слабосвязанными командами разработчиков". Я очень быстро забываю контекст проекта. Через полгода уже не помню ничего, если в течение этого времени не заглядывал код. Так что "я сейчас" и "я через год" - это и есть слабосвязанные команды разработчиков :)

А в принципе, весь IoC для этого был придуман. DI - лишь один из вариантов.

Смотрите, я привёл код, который "на пальцах" демонстрирует идею позднего связывания и создания JS-кода без статических импортов. А вы говорите, дайте "нормальный пример". Я уже дал. Просто вы не видите, где его можно применить. Я дам вам другой пример (вот интерфейс, вот место применения, а вот - имплементация в другом пакете), и вы скажете, что это же можно сделать и со статическими импортами. И будете совершенно правы. А кто-то скажет, что это же можно сделать и на любом другом ЯП или даже сразу в машкодах. И тоже будет совершенно прав.

Дело не в правоте, а в возможности решать задачи наиболее удобным для вас путём. Если вы не видите, где применять JS-код без статических импортов или использовать позднее связывание - вам просто это не нужно. Было бы нужно, вы бы зацепились глазом за пример выше и вас бы сразу триггернуло. Я вас уверяю, это и есть нормальный пример. Там всё видно. По крайней мере тем, кому это надо или может быть надо.

DI - это про внедрение зависимостей. Зависимости есть в любом коде, состоящем из более, чем одного файла. Статические импорты в JS - это тоже зависимости.

DI в JS можно вообще без классов использовать:

// src/App/Config.mjs

export default () => ({
    greeting: 'Hello',
    punctuation: '!',
});
// src/App/FormatName.mjs

export default () => (name) => {
    const value = name.trim().toLowerCase();
    return value.charAt(0).toUpperCase() + value.slice(1);
};
// src/App/Greet.mjs

export const __deps__ = {
    default: {
        config: 'App_Config$',
        formatName: 'App_FormatName$',
    },
};

export default ({config, formatName}) =>
    (name) => `${config.greeting}, ${formatName(name)}${config.punctuation}`;
// bootstrap.mjs

import Container from '@teqfw/di';

const container = new Container();

container.addNamespaceRoot(
    'App_',
    new URL('./src/App/', import.meta.url).href,
    '.mjs'
);

const greet = await container.get('App_Greet$');

console.log(greet('  aLEX  '));

Вот архив с кодом - достаточно распаковать, затем npm install & npm start.

Если кодовая база большая, то нужен какой-то механизм для связывания отдельных файлов. В JS для этого используют импорты - статические и динамические. DI - это как раз и есть связывание отдельных исходников в единую кодовую базу через динамический импорт. ООП тут вообще перпендикулярно стоит ко всему этому.

DI-контейнер - это никакая не революция. Им сто лет в обед. Я со Spring'ом работал в 2010-х. Это уже была 3-я версия платформы. Затем я использовал DI в PHP - в Magento. Очень своеобразный DI, особенно в первой версии (2008 - 2015 год). Использовал DI из Symfony 2. В PHP вообще с этим делом всё хорошо, даже стандарт есть - PSR-11.

Когда окончательно перешёл в JS даже "запилил" свой DI - чтобы писать в привычной для себя манере. Правда я позволил ИИ его "под себя" переписать (под "силиконовых", а не под "кожаных").

Что касается примеров кода, то могу предложить посмотреть вот этот код - @flancer32/teq-cms. Я писал его "ручками", ещё на "рукописном" DI. А код в @flancer32/teq-web уже написан ИИ (Codex) с "его" DI. Я в код заглядывал, но руками старался не править ничего. Самый, что ни на есть "машинный код".

Рабочий код на фронте - вот. Зайдите в DevTools и посмотрите, что видит браузер:

То же самое, что видит разработчик в IDE
То же самое, что видит разработчик в IDE

Это старый код, но сделан тоже в той же IoC-парадигме с моим рукописным DI в основе. Для построения UI используется vue и Quasar.

Если вы посмотрите все es6-модули, то там не будет ни одного (!) статического импорта. Статические импорты нужны в головном скрипте для загрузки DI-библиотеки, а дальше контейнер сам динамически (через import()) загружает нужные зависимости по мере необходимости. Как следствие, код становится изоморфным (без изменений может использоваться как в браузере, так и в nodejs). Вот вообще без изменений! Поначалу я на бэке импортировал node'овские библиотеки, но сейчас и они инжектятся. То есть, можно и бэкендовый код в браузере запускать, только node-библиотеки нужно замокать (делается через препроцессор контейнера). А сам код приложения остаётся без изменений.

И в отличие от многих других DI-контейнеров (включая контейнер автора статьи), у моего контейнера ограничена возможность что-то зарегистрировать специально:

const container = new Container()
  .registerValue(ID_GENERATOR, randomUUID)
  .registerValue(CLOCK, () => new Date())
  .registerClass(CartApplicationService);

У меня регистрировать можно только если перевести контейнер в тестовый режим:

container.enableTestMode();
container.register("App_Service$", mockService);

То есть, в prod-режиме все es6-модули резолвятся и подгружаются автоматически, сколько бы их там ни было. Нужно только "потянуть" головной модуль, а дальше строится дерево зависимостей и подтягиваются исходники (можно прямо с unpkg.com тянуть). Более того, с учётом наличия в контейнере препроцессора, можно менять маппинг идентификаторов зависимостей на исходный код и реализовывать такие посторонние для "ванильного" JS вещи, как интерфейсы. А с учётом наличия там же постпроцессора, можно использовать в JS и AOP.

Все эти приёмы и технологии давным давно используются в программировании и никакая не революция. Просто они используются там, где кодовая база проекта начинает заваливать за миллионы строк кода (в Magento 2, например, больше 2 млн. и для работы с JS используется requirejs, тоже своего рода DI).

Много у вас примеров таких объёмных кодовых баз именно на JavaScript? А для кодирования на одной веб-страничке чего угодно такие навороты действительно не нужны.

IMHO, всё сильно поменялось, а чувак сделал глобальный вывод на основе своих локальных про... махов - "миддлы не вытаскивают!!".

А может пора уже в новых условиях всю эту "табель о рангах" вместе со сторипоинтами заменить на что-то другое? На те же токены, например, тип модели и уровень reasoning'а? На одной и той же задаче 5.6 Sol на light reasoning даст результат, очень отличающийся от результата 5.6 Luna на high reasoning. И не всегда в лучшую сторону.

Про "сделать видимым контур проверки результата" - мысль дельная. Но об этом писали буквально в первых публичных версиях GPT: "ChatGPT может допускать ошибки. Рекомендуем проверять важную информацию."

В целом - статья годная и полезная. Вопрос - уже половина ответа.

А почему только для Codex? Разве другие агенты не работают с "распределённым AGENTS.md" таким же образом? Лично я в основном работаю именно с Codex'ом. Немного работал с opencode, но там я визуально не контролировал, что агент считывал всю иерархию AGENT.md до целевого каталога (с Codex'ом я это проверял). Думал, что все агенты действуют по такому же принципу - считывают все AGENTS.md от корня проекта и до рабочего каталога. Это не так?

UPDATE:

Проверил opencode. На промпт "Сколько файлов AGENTS.md участвует в формировании инструкций для этого каталога. Перечисли все." Получил такой ответ:

Четыре файла AGENTS.md образуют иерархию инструкций:

/home/alex/work/flancer32/alarisa/AGENTS.md — корневой уровень продукта; предписывает консультироваться с ctx/ и ctx/docs/.
/home/alex/work/flancer32/alarisa/ctx/AGENTS.md — корень когнитивного контекста; направляет читать docs/.
/home/alex/work/flancer32/alarisa/ctx/docs/AGENTS.md — уровень проектной документации; перечисляет product/ как один из разделов.
/home/alex/work/flancer32/alarisa/ctx/docs/product/AGENTS.md — уровень документации продукта; содержит прямые правила для работы с файлами в этом каталоге.

"Этот каталог" бы указан ранее.

Похоже, что opencode тоже видит файлы AGENTS.md в иерархии. Что логично, по такому же принципу работают конфигурационные файлы .htaccess для apache.

UPDATE2:

Это для Copilot'а:

Copilot также перечислил все AGENTS.md, лежащие в иерархии на пути к целевому каталогу. Каждый AGENTS.md содержит инструкции для своего уровня и в совокупности собираются в единый пул инструкций для рабочего каталога.

  Сколько строк у вас в AGENTS.md или CLAUDE.md?

Вот у меня отдельный AGENTS.md на каждый важный для проекта каталог (112 строк для корневого). Я уже про это писал. Агенты (codex как минимум) читают все AGENTS.md от корня проекта к рабочему (для агента) каталогу. Правила для агентов можно (и нужно!) "размазывать" по всему проекту.

Я долгое время работал с Magento, строил на её базе магазины и интегрировал в них различные расширения сторонних производителей.

Я откушал полной ложкой то, что вы называете "their own medicine". А вот вы в это время "ели" что-то другое.

"Мы есть то, что мы едим." (с)

проверить != обучить

А так всё верно. Самый надёжный способ проверить, что человек умеет плавать - бросить его в воду. Все, кто выплыл - 100% умеют.

Какая "сотня активных пользователей"?!

План клиента — выйти на 20 (магазинов в год).

Там один клиент, которому надо открыть в 4 раза больше магазинов за год, чем он это делал вручную. Если клиент способен оплатить 6 месяцев работы команды из 3 человек и кучи агентов, а в ответ получает выполнение своих планов ("закрытие боли"), то... какая там разница что за код нагенерировал компилятор ИИ, если код выполняет свою работу?

Кстати, ИИ вполне себе нормально код генерирует, он архитектуру не держит. А код сгенерирует в любом стиле, особенно если есть скилл под этот стиль.

Продукт ответил на вопрос бизнеса: сказал, где строить, — и клиент пошёл строить. 

Это не стартап, а одноразовое приложение, для одного клиента: "Продукт ответил на вопрос бизнеса: сказал, где строить, — и клиент пошёл строить".

Для следующего клиента нужно уже на этих же инструментах и методологии писать другое приложение.

Из очевидного: доработка таких "одноразовых приложений" до новых нужд клиента - довольно дешёвый процесс:

Главное наблюдение: чем больше контекста накапливалось, тем быстрее шла разработка. В какой-то момент стало достаточно сказать «добавь в CRM такую-то сущность» — и агент по накопленным примерам сам создавал таблицу, сервис, контроллеры и фронтенд, и всё это вписывалось в дизайн продукта.

Отсюда вывод - ценность разработки смещается не в код, а в документацию к коду.

С нетерпением жду продолжение про шаринг библиотек и изоляцию зависимостей.

А знаете где эта проблема уже решена на уровне архитектуры? В языке, который не рекомендуют использовать для создания браузерных приложений - в JavaScript.

клиент докупает необходимые продукты, система расширяется в рантайме.

Чистейший late binding! Приятно видеть, что технологии "кровавого энтерпрайза" прорастают в браузерных приложениях.

Да, несколько непонятно, что именно нужно делать в игре. Нужно разбираться методом проб и ошибок. Но в целом - впечатляет (y) Интересно, а насколько долго в неё можно играть? Насколько большая "игровая Вселенная"? Если это "браузерка", то все возможные повороты сценария должны быть предопределены и загружены. Если, в теории, я выполню все квесты, то - всё?

Слоистый AGENTS.md разложен по подпапкам, поэтому в контекст подтягивается ровно то, что относится к текущему модулю, а не вся документация разом.

Я называл такой подход "иерархией AGENTS.md", "слоистый" - интересный подбор терминов. Необычный, как по мне. В любом случае, ваш опыт также подтверждает, что множественное использование AGENTS.md в одном проекте - вполне себе практическое решение. А то мне за ту статью минусов в панамку напихали, типа нейрослоп. Ну, да - нейрослоп. Специально сжатый. Не умеем мы ещё читать нейрослоп. Даже специально сжатый.

Это так, мысли вслух. Удачи в эволюции приложения!

Information

Rating
3,253-rd
Location
Рига, Латвия, Латвия
Date of birth
Registered
Activity

Specialization

Фулстек разработчик, Архитектор программного обеспечения
Ведущий
JavaScript
HTML
CSS
Node.js
Vue.js
Веб-разработка
Progressive Web Apps
PostgreSQL
MySQL
GitHub