Direct Control
DC — модуль сам контролирует к каким конкретно реализациям ему обращаться.
class Form extends Object { scope = 'Form' @mem get log() { return new ConsoleLog( this.scope ) } // direct @mem get net() { return new HttpNet( this.scope ) } // direct send( payload: string ) { this.log.info( 'Sending', payload ) this.net.request( payload ) } } class SignIn extends Form { scope = 'SignIn:' + this.auth.id + ':' + super.scope @mem get auth() { return new LocalAuth( this.scope ) } // direct submit() { this.send( this.auth.key ) this.log.info( 'Signed in', this.auth.id ) } } new SignIn().submit() // production run
✅ Быстрая работа, хорошо оптимизируется компилятором благодаря инлайнингу.
✅ Простой код, не требующий когнитивных усилий.
✅ Удобно навигироваться по коду, переходя по объявлениям/упоминаниям сущностей.
❌ Сложно использовать один и тот же код, но с разными реализациями зависимостей.
Для борьбы с последним пунктом применяются различные техники инверсии контроля.
Inversion of Control
IoC — модуль не знает, какую именно реализацию он вызывает, так как это контролируется внешним кодом.
✅ Один и тот же код может работать с неограниченным набором реализаций.
❌ Косвенная адресация осложняет оптимизацию компилятором.
❌ Навигация по коду осложнена тем, что в одной точке вызова могут запускаться самые разные реализации.
Далее идут разные подходы к инверсии контроля, с примерами, где единожды написанный код может работать с разными логгерами/транспортами/кешами/аутентификаторами.
Dependency Injection
DI — предоставление публичного API, через которое внешний код должен предоставить свою реализацию.
Типичные паттерны: Callback, Command, Observer.
❌ Все зависимости с любой глубины должны подниматься в публичный интерфейс.
❌ Владелец должен предоставить все зависимости для всего дерева модулей.
✅ В публичном интерфейсе сразу видно все зависимости, как бы глубоко они ни использовались.
Argument Dependency Injection
aDI — владелец обязан предоставить реализации через аргументы функции/метода.
class Form extends Object { send( payload: string, makeLog: ( scope: string )=> Log, // factory makeNet: ( makeLog: ( scope: string )=> Log )=> Net, // factory param ) { const log = makeLog( 'Form' ) const net = makeNet( log.maker ) // pass through log.info( 'Sending', payload ) HttpNet.request( this.scope, payload ) // pass through } } class SignIn extends Form { submit( auth: Auth, // instance makeLog: ( scope: string )=> Log, // factory makeNet: ( makeLog: ( scope: string )=> Log )=> Net, // factory param ) { const log = makeLog( 'SignIn:' + auth.id ) this.send( auth.key, log.maker, makeNet ) // pass through log.info( 'Signed in', auth.id ) } } new SignIn().submit( new FakeAuth, scope => new MemLog( scope ), makeLog => new LocalNet( new MemCache( makeLog ), makeLog ), ) // isolated run
✅ Можно точно контролировать какие зависимости будут использоваться в каждой функции/методе.
❌ Зависимости необходимо вручную прокидывать при каждом вызове метода/функции.
❌ Изменение состава зависимостей предка приводит к каскадным изменениям сигнатур методов потомков.
Constructor Dependency Injection
cDI — владелец обязан предоставить реализации через аргументы конструктора.
class Form extends Object { constructor( private makeLog: ( scope: string )=> Log, // factory private makeNet: ( makeLog: ( scope: string )=> Log )=> Net, // factory param ) { super() this.log = makeLog( 'Form' ) this.net = makeNet( this.log.maker ) // pass through } log: Log net: Net send( payload: string ) { this.log.info( 'Sending', payload ) this.net.request( payload ) } } class SignIn extends Form { constructor( private auth: Auth, // instance private makeLog: ( scope: string )=> Log, // factory private makeNet: ( makeLog: ( scope: string )=> Log )=> Net, // factory param ) { const log = makeLog( 'SignIn:' + auth.id ) super( log.maker, makeNet ) // pass through } submit() { this.send( this.auth.key ) this.log.info( 'Signed in', this.auth.id ) } } new SignIn( new FakeAuth, scope => new MemLog( scope ), makeLog => new LocalNet( new MemCache( makeLog ), makeLog ), ).submit() // isolated run
✅ О проблемах с созданием зависимостей узнаешь в точке конструирования зависимого.
❌ Зависимости необходимо вручную прокидывать при каждом создании объекта.
❌ Созданная для конструктора зависимость со всеми своими зависимостями может так и не понадобиться.
❌ Изменение состава зависимостей предка приводит к каскадным изменениям сигнатур конструкторов потомков.
❌ Конструктор не может содержать асинхронную логику.
Property Dependency Injection
pDI — владелец обязан предоставить реализации через свойства/поля/атрибуты объекта/структуры.
class Form extends Object { Log: new( scope: string )=> Log = ConsoleLog, // default Net: new()=> Net = HttpNet, // default NetCache: new()=> Cache = FileCache, // default scope = 'Form' @mem get log() { return new this.Log( this.scope ) } @mem get net() { const net = new this.Net() net.Log = this.log.maker // pass through net.Cache = this.NetCache // pass through return net } send( payload: string ) { this.log.info( 'Sending', payload ) this.net.request( payload ) } } class SignIn extends Form { Auth: new()=> Auth = LocalAuth // default scope = 'SignIn:' + this.auth.id + ':' + super.scope @mem get auth() { const auth = new this.Auth() auth.Log = this.log.maker // pass through return auth } submit() { this.send( this.auth.key ) this.log.info( 'Signed in', this.auth.id ) } } const form = new SignIn form.Log = MemLog form.Net = LocalNet form.Auth = FakeAuth form.NetCache = MemCache form.submit() // isolated run
✅ При изменении состава зависимостей предка, нет необходимости менять код потомков.
❌ Легко забыть передать какую‑либо зависимость и она со всеми своими зависимостями станет неконтролируемой.
❌ Дефолтная реализация со всеми зависимостями попадает в бандл, даже если не используется.
Dependency Lookup
DL — Использование внешнего API для поиска реализации по требованию.
⭕ Чтобы узнать все косвенные зависимости нужен тулинг, но необходимости в этом знании обычно нет.
✅ Владельцу нет необходимости знать и предоставлять все зависимости, используемые в глубине, так как есть глобальные дефолтные реализации.
✅ Владелец может при необходимости перебить любые зависимости, как бы глубоко они ни использовались.
✅ Можно иметь несколько групп реализаций, пригодных для разных контекстов использования, уровней изоляции и тп.
✅ Дефолтная реализация зависимости для разных глобальных контекстов задаётся в одном месте — рядом с её интерфейсом.
Global Dependency Lookup
gDL — Обращение к общему глобальному состоянию.
Типичные паттерны: Singleton, Service Locator, Application Store.
namespace App { export class Form extends Object { log = App.Log.make( 'Form' ) send( payload: string ) { return App.override( { // temporary global change Log: this.log }, ()=> { App.Log.info( 'Sending', payload ) App.Net.request( payload ) } ) } } export class SignIn extends Form { log = super.log.make( 'SignIn:' + App.Auth.id ) submit() { return App.override( { // temporary global change Log: this.log }, ()=> { this.send( App.Auth.key ) App.Log.info( 'Signed in', App.Auth.id ) } ) } } } App.runIsolated( ()=> new App.SignIn().submit() ) // isolated run
❌ Изменение глобального состояния меняет реализацию сразу для всего приложения, что может дать неожиданные сайд эффекты.
❌ Не совместимо с асинхронными функциями, если в рантайме нет поддержки асинхронного контекста.
❌ Много бойлерплейта с оверрайдами в прикладной логике.
Context Dependency Lookup
cDL — Обращение через локальный контекст, инжектируемый из вне.
Типичные паттерны: Context Tree, Environment Variables, Injection Context.
namespace DefaultContext { export class Form extends Thing { @mem() get $() { return super.$.context({ Log: super.$.Log.config({ scope = ()=> this.scope() // properties binding }), }) } @mem scope() { return 'Form' } @mem log() { return new this.$.Log() } // implicit context inject @mem net() { return new this.$.Net() } // implicit context inject send( payload: string ) { this.log().info( 'Sending', payload ) this.net().request( payload ) } } export class SignIn extends Form { @mem scope() { return 'SignIn:' + this.auth().id() + ':' + super.scope() } @mem auth() { return new this.$.Auth } // implicit context inject submit() { this.send( this.auth().key() ) this.log().info( 'Signed in', this.auth().id() ) } } new IsolatedContext.SignIn().submit() // isolated run }
✅ Каждый объект/функция может отнаследовать свой контекст от внешнего, и использовать его для всех своих зависимостей.
✅ Легко связывать значения в контексте с локальными свойствами объекта.
✅ Минимум бойлерплейта с незначительным визуальным шумом.
Сравнение
Характеристика \ Подход |
DC |
aDI |
cDI |
pDI |
gDL |
cDL |
Легко перейти от места вызова к коду реализации. |
✅ |
❌ |
❌ |
❌ |
❌ |
❌ |
Код хорошо оптимизируется компилятором благодаря мономорфности. |
✅ |
❌ |
❌ |
❌ |
❌ |
❌ |
Модуль не тянет с собой в бандл реализации по умолчанию. |
❌ |
✅ |
✅ |
❌ |
❌ |
❌ |
В разных местах использования один и тот же код может работать с разными зависимостями. |
❌ |
✅ |
✅ |
✅ |
✅ |
✅ |
Владелец может настроить, какие зависимости будут использованы на любом уровне вложенности. |
❌ |
✅ |
✅ |
⭕ |
✅ |
✅ |
Владелец не обязан знать обо всех зависимостях в глубине вызовов. |
✅ |
❌ |
❌ |
❌ |
✅ |
✅ |
Зависимости не нужно вручную прокидывать при каждом вызове конструктора/метода/функции. |
✅ |
❌ |
❌ |
❌ |
✅ |
✅ |
Дерево зависимостей может прозрачно разрушаться автоматически и создаваться на лету. |
✅ |
❌ |
❌ |
❌ |
❌ |
✅ |
При изменении зависимостей родителя, нет необходимости менять код всех потомков. |
✅ |
❌ |
❌ |
✅ |
✅ |
✅ |
Прикладной код простой и не завален техническим бойлерплейтом. |
✅ |
❌ |
❌ |
❌ |
❌ |
✅ |
В $mol выбран подход cDL как наиболее простой в поддержке, но в то же время гарантирующий возможность детальной кастомизации из вне со строгими гарантиями статической типизации.
Комментарии (21)

is1
30.08.2026 19:45cDL — Обращение через локальный контекст, инжектируемый из вне.
Это подгон определения под твоё решение. Локальный контекст может не инжектироваться, а лежать в AsyncLocalStorage.
Я на днях навайбкодил библиотеку (ripple-di) как раз в таком стиле: вместо
this.$берется асинхронный контекст и строится автоматический граф зависимостей при локальных оверрайдах.
nin-jin Автор
30.08.2026 19:45Это gDL.

is1
30.08.2026 19:45Не совсем. Сами зависимости (перечень изменяемых элементов) — глобальны, но их значения — полностью локальны внутри асинхронного контекста. Аргумент “Изменение глобального состояния меняет реализацию сразу для всего приложения” невалиден: подмены глобального состояния в принципе нет. Равно как и аргумент “Много бойлерплейта с оверрайдами в прикладной логике” — это соломенное чучело, бойлерплейта нет ВООБЩЕ: вызовы зависимостей имеют ноль бойлерплейта (это просто вызовы функций), а временная подмена — это максимально сжатый и понятный код, который читается буквально как “подменить X на X’ и вызвать функцию”.
Вот конкретно твой пример переписан:
// Реализации по умолчанию (опционально): const Log = defineFactoryDependency( (scope: string) => new ConsoleLog(scope), ) const Net = defineFactoryDependency( (scope: string) => new HttpNet(scope), ) const Auth = defineFactoryDependency( (scope: string) => new LocalAuth(scope), ) class Form extends Object { scope = 'Form' @mem get log() { return Log(this.scope) } // context lookup @mem get net() { return Net(this.scope) } // context lookup send(payload: string) { this.log.info('Sending', payload) this.net.request(payload) } } class SignIn extends Form { scope = 'SignIn:' + this.auth.id + ':' + super.scope @mem get auth() { return Auth(this.scope) } // context lookup submit() { this.send(this.auth.key) this.log.info('Signed in', this.auth.id) } } // Оверрайды на время запуска функции: await withOverrides([ provide(Log, scope => new MemLog(scope)), provide(Net, scope => new LocalNet(scope)), provide(Auth, scope => new FakeAuth(scope)), ], () => new SignIn().submit()) // isolated runВ прикладном коде при этом нет ни
this.$, ни прокидывания контекста, ни временных глобальных оверрайдов, которые “могут дать неожиданные сайд эффекты”.
nin-jin Автор
30.08.2026 19:45Создал объект в одном контексте, а обратился к его методу из другого - вот тебе и неожиданный сайд эффект. А с мемоизацией, зависящих от стека вызова, геттеров совсем веселье будет.

is1
30.08.2026 19:45Создал объект в одном контексте, а обратился к его методу из другого
Такой футган можно в любой DI-архитектуре допустить. Не надо зависимости передавать и сохранять случайным образом, всё равно есть какая-то конвенция на lifecycle, вопрос в том сильно ли она церемониальная или нет.
При “нормальной” работе (нужна зависимость - позвал геттер, сложил в локальную переменную) такой проблемы нет и не будет.
А с мемоизацией, зависящих от стека вызова
Мемоизацию просто тоже нужно иметь scope-aware. Я добавил, ради прикола: и как декоратор для методов, и как standalone memoize хелпер.
В любом случае, мой поинт в том, что так или иначе между gDL и cDL есть ещё вариант ambient/async scoped lookup, который в заявленную “непротиворечивую” классификацию не помещается. В ripple один и тот же объект при вызове из разных async scopes намеренно видит разные зависимости, при этом параллельные потоки друг на друга не влияют, и в принципе все указанные в табличке для gDL минусы (кроме необходимости асинхронного рантайма) отсутствуют.

nin-jin Автор
30.08.2026 19:45Такой футган можно в любой DI-архитектуре допустить.
В cDI, pDI, cDL не получится. Впрочем, славно, что вы признали существование проблемы.
все указанные в табличке для gDL минусы (кроме необходимости асинхронного рантайма) отсутствуют
А, нет, не признали. Предлагаю продолжить, когда вы свой нейрослоп опробуете в бою.

funca
30.08.2026 19:45cDL тайпчекается статически?

nin-jin Автор
30.08.2026 19:45Разумеется.

funca
30.08.2026 19:45Расскажите подробнее про то, как это реализовано. По коду контекст выглядит как обычный словарь.

nin-jin Автор
30.08.2026 19:45https://page.hyoo.ru/#!=z90h0r_m6qkvl
Ну и на Ангуляр ссылка в статье есть.

funca
30.08.2026 19:45Если тип контекста описывается в декларациях неймспейса (то есть глобально), тогда тип у this.$ будет общим на весь проект? Чем-то напоминает модули (которые "import ... from").

vanxant
30.08.2026 19:45Главный минус cDI в том, что конструкторы тащем-то не для зависимостей придумали. У объектов знаете ли бывают разные там параметры и прочая полезная нагрузка...
nronnie
Нейрослопщики уже ленятся даже списки с LLM-иконками переделать в обычный текстовый формат.
И “классификация” в статье - говно. Стандартная классификация IoC это:
DI
Callbacks
Events
А тут не классификация, а какая-то мешанина.
nin-jin Автор
Колбэки - это буквально aDI. А вот события - это уже eDI. Про него как-то подзабыл и ни один нейрослопер не напомнил.
Sazonov
Так всё-таки, расскажите, почему текст пестрит иконками? У вас хотели для них настроены или вы каждый раз делаете автозамену? Просто такой подход - это очень яркий признак того, что текст сгенерирован llm.
cmyser
У Дмитрия они всегда были, задолго до ллм. Причем эти же самые иконки)
Да и тут их всего три вида
Иконки не являются хорошим признаком для определения слопа
https://t.me/digital_ninjaa/337
LyuMih
Почитайте другие статьи автора - там везде одинаковый стиль с иконками уже на протяжении многих лет
Sazonov
Да, я почитал. Правда уже после того как написал комментарий. В других статьях автора радует большее разнообразие :)
Но я рад что был неправ и встретил исключение из правил.
nin-jin Автор
Я просто сделал хоткеи в редакторе.
И научил нейронку их нажимать.flancer
Это авторский стиль. Чтобы каждому было сразу понятно что "$mol всегда попадает в яблочко"! И он действительно сложился ещё до появления нейронок.