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 как наиболее простой в поддержке, но в то же время гарантирующий возможность детальной кастомизации из вне со строгими гарантиями статической типизации.
nronnie
Нейрослопщики уже ленятся даже списки с LLM-иконками переделать в обычный текстовый формат.
И “классификация” в статье - говно. Стандартная классификация IoC это:
DI
Callbacks
Events
А тут не классификация, а какая-то мешанина.
nin-jin Автор
Колбэки - это буквально aDI. А вот события - это уже eDI. Про него как-то подзабыл и ни один нейрослопер не напомнил.