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

❌ Все зависимости с любой глубины должны подниматься в публичный интерфейс.

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

✅ В публичном интерфейсе сразу видно все зависимости, как бы глубоко они ни использовались.

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.

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


  1. nronnie
    30.08.2026 19:45

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

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

    • DI

    • Callbacks

    • Events

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


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

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