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 как наиболее простой в поддержке, но в то же время гарантирующий возможность детальной кастомизации из вне со строгими гарантиями статической типизации.


Актуальный оригинал на $hyoo_page.

Комментарии (21)


  1. nronnie
    30.08.2026 19:45

    Нейрослопщики уже ленятся даже списки с LLM-иконками переделать в обычный текстовый формат.

    И “классификация” в статье - говно. Стандартная классификация IoC это:

    • DI

    • Callbacks

    • Events

    А тут не классификация, а какая-то мешанина.


    1. nin-jin Автор
      30.08.2026 19:45

      Колбэки - это буквально aDI. А вот события - это уже eDI. Про него как-то подзабыл и ни один нейрослопер не напомнил.


      1. Sazonov
        30.08.2026 19:45

        Так всё-таки, расскажите, почему текст пестрит иконками? У вас хотели для них настроены или вы каждый раз делаете автозамену? Просто такой подход - это очень яркий признак того, что текст сгенерирован llm.


        1. cmyser
          30.08.2026 19:45

          У Дмитрия они всегда были, задолго до ллм. Причем эти же самые иконки)

          Да и тут их всего три вида

          Иконки не являются хорошим признаком для определения слопа

          https://t.me/digital_ninjaa/337


        1. LyuMih
          30.08.2026 19:45

          Почитайте другие статьи автора - там везде одинаковый стиль с иконками уже на протяжении многих лет


          1. Sazonov
            30.08.2026 19:45

            Да, я почитал. Правда уже после того как написал комментарий. В других статьях автора радует большее разнообразие :)

            Но я рад что был неправ и встретил исключение из правил.


        1. nin-jin Автор
          30.08.2026 19:45

          Я просто сделал хоткеи в редакторе. И научил нейронку их нажимать.


        1. flancer
          30.08.2026 19:45

          Это авторский стиль. Чтобы каждому было сразу понятно что "$mol всегда попадает в яблочко"! И он действительно сложился ещё до появления нейронок.


  1. is1
    30.08.2026 19:45

    cDL — Обращение через локальный контекст, инжектируемый из вне.

    Это подгон определения под твоё решение. Локальный контекст может не инжектироваться, а лежать в AsyncLocalStorage.

    Я на днях навайбкодил библиотеку (ripple-di) как раз в таком стиле: вместо this.$ берется асинхронный контекст и строится автоматический граф зависимостей при локальных оверрайдах.


    1. nin-jin Автор
      30.08.2026 19:45

      Это gDL.


      1. 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.$, ни прокидывания контекста, ни временных глобальных оверрайдов, которые “могут дать неожиданные сайд эффекты”.


        1. nin-jin Автор
          30.08.2026 19:45

          Создал объект в одном контексте, а обратился к его методу из другого - вот тебе и неожиданный сайд эффект. А с мемоизацией, зависящих от стека вызова, геттеров совсем веселье будет.


          1. is1
            30.08.2026 19:45

            Создал объект в одном контексте, а обратился к его методу из другого

            Такой футган можно в любой DI-архитектуре допустить. Не надо зависимости передавать и сохранять случайным образом, всё равно есть какая-то конвенция на lifecycle, вопрос в том сильно ли она церемониальная или нет.

            При “нормальной” работе (нужна зависимость - позвал геттер, сложил в локальную переменную) такой проблемы нет и не будет.

            А с мемоизацией, зависящих от стека вызова

            Мемоизацию просто тоже нужно иметь scope-aware. Я добавил, ради прикола: и как декоратор для методов, и как standalone memoize хелпер.

            В любом случае, мой поинт в том, что так или иначе между gDL и cDL есть ещё вариант ambient/async scoped lookup, который в заявленную “непротиворечивую” классификацию не помещается. В ripple один и тот же объект при вызове из разных async scopes намеренно видит разные зависимости, при этом параллельные потоки друг на друга не влияют, и в принципе все указанные в табличке для gDL минусы (кроме необходимости асинхронного рантайма) отсутствуют.


            1. nin-jin Автор
              30.08.2026 19:45

              Такой футган можно в любой DI-архитектуре допустить.

              В cDI, pDI, cDL не получится. Впрочем, славно, что вы признали существование проблемы.

              все указанные в табличке для gDL минусы (кроме необходимости асинхронного рантайма) отсутствуют

              А, нет, не признали. Предлагаю продолжить, когда вы свой нейрослоп опробуете в бою.


  1. funca
    30.08.2026 19:45

    cDL тайпчекается статически?


    1. nin-jin Автор
      30.08.2026 19:45

      Разумеется.


      1. funca
        30.08.2026 19:45

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


        1. nin-jin Автор
          30.08.2026 19:45

          https://page.hyoo.ru/#!=z90h0r_m6qkvl

          Ну и на Ангуляр ссылка в статье есть.


          1. funca
            30.08.2026 19:45

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


            1. nin-jin Автор
              30.08.2026 19:45

  1. vanxant
    30.08.2026 19:45

    Главный минус cDI в том, что конструкторы тащем-то не для зависимостей придумали. У объектов знаете ли бывают разные там параметры и прочая полезная нагрузка...