Pull to refresh

Comments 13

"Ничего не понятно, но очень интересно!" (с)

Я ничего не понимаю в Котлине, но почему бы не сделать так, без рефлексии, аннотаций, кодогенерации и прочего?

Реализуем простейший IoC контейнер с двумя контекстами: обычный и изолированный.

open class Ambient(
    val builders: MutableMap< Any, ( Ambient )-> Any > = mutableMapOf(),
    val donor: Ambient? = null,
) {
    
    companion object {
    	val default = Ambient()
    	val isolated = default.derive()
    }
    
    val cache: MutableMap< Any, Any > = mutableMapOf()
    
    fun builder( Klass: Any, builder: ( Ambient )-> Any )
        = builders.put( Klass, builder )
    
    fun <Val> make( factory: Factory< Val > ): Val {
        
        var cur: Ambient? = this
        while( cur != null ) {
        	val build = cur.builders.get( factory )
        	if( build != null ) return build( this ) as Val
            cur = cur.donor
        }
        
        return factory.build( this )
    }
    
    fun <Val> get( factory: Factory< Val > ): Val {
        
        val cached = cache.get( factory )
        if( cached !== null ) return cached as Val
        
        val maked = this.make( factory )
        cache.put( factory, maked as Any )
        
        return maked
    }
    
    fun derive( vararg builders: Pair< Any, ( Ambient )-> Any > )
    	= Ambient( mutableMapOf( *builders ), this )
    
}

abstract class Factory< Val > {
    abstract fun build( ambient: Ambient ): Val
}

Теперь реализуем класс с некоторой дефолтной реализацией и фабрикой:

open class Activity( val ambient: Ambient ) {
    open var name = "Default Activity"
    companion object: Factory< Activity >() {
        // Default deps builder
        override fun build( ambient: Ambient ) = Activity( ambient )
    }
}

Теперь реализуем использующие его класс, где дефолтная реализация выбирается динамически и тут же добавляем мок в изолированный контекст:

open class Talk( val ambient: Ambient ) {
    companion object: Factory< Talk >() {
        override fun build( ambient: Ambient )
        	// Dynamic deps selection by default
        	= if( (0..1).random() == 1 ) Hello( ambient ) else Bye( ambient )
        init {
            Ambient.isolated.builder( Talk, { ambient -> Talk( ambient ) } )
        }
    }
    open fun say() {}
}

open class Hello( ambient: Ambient ): Talk( ambient ) {
    override fun say() {
        println( "Hello, " + ambient.get( Activity ).name )
    }
}

open class Bye( ambient: Ambient ): Talk( ambient ) {
    override fun say() {
        println( "Bye, " + ambient.get( Activity ).name )
    }
}

Теперь реализуем апи, которое не планируем доставать из контекста, но которое сможет этот контекст использовать:

open class MyApi( val ambient: Ambient ) {
    fun run() {
        ambient.get( Talk ).say()
    }
}

А теперь запустим его в изолированном контексте (ничего не выведется), и в контексте с переопределённой глубокой зависимостью (выведет либо "Hello, Home", либо "Bye, Home"):

open class Home( ambient: Ambient ): Activity( ambient ) {
    override var name = "Home"
}

fun main() {
    
    MyApi( Ambient.isolated ).run() // no print
    
    // Custom ambient context with overrides
    val ambient = Ambient.default.derive(
        Activity to { ambient -> Home( ambient ) }
	)
    
    MyApi( ambient ).run() // [Hello|Bye], Home
    
}

Да, DI можно организовывать очень по-разному, в зависимости от потребностей и архитектуры приложения. Если я верно прочитал предложенный код, то принципиально такое же решение у нас и было, оно было описано в статье в разделе про историю и IoContainer: иерархия словарей стратегий (builder) как создавать зависимости. Вместо companion-object мы использовали Class<?>. Предложенное в комментарии решение предполагает, что мы вручную достаём из прокинутогоAmbient все зависимости, вместо того, чтобы рефлексийно получить информацию о параметрах @Inject конструктора и автоматически внедрять зависимости. Насколько я понял, проблему с опциональными зависимостями предлагается решать через "дефолтные" реализации зависимостей: нет явного оверрайда - может использоваться no-op реализация. Это хороший подход, но как было описано, для нашего проекта было бы очень тяжело формулировать такие no-op реализации, в то время как для других проектов это может быть хорошим вариантом. Так что в задачи DI у нас входила возможность предоставления зависимостей в форме Optional<T>. Так же, у нас в проекте до сих пор очень много Java кода.

Касательно производительности предложенного решения в рамках очень больших графов так же могут быть вопросы (много лямбд, companion-object), такое решение может в некоторых случаях работать не быстрее чем на рефлексии, как показывает практика.

Итого, делать можно по-разному, каждый выбирает, что ему больше подходит. Спасибо, очень интересный код на idiomatic Kotlin для сравнения!

Совсем не понятно, чем Optional может быть проще, чем no-op, даже с учётом легаси. Просто никогда не будет приходить null - можно даже Optional не убирать из легаси.

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

Кроме того, ручное доставание объектов из контекста даёт создание объектов по требованию, а не создание пол мира только лишь для создания объекта, даже если ему эти объекты вообще не понадобятся.

Ну и бонусом - не надо при наследовании заниматься ручным объявлением всех зависимостей предка и передачей их ему.

Спасибо за статью.

KSP у нас не давал больших профитов по сравнению с kapt (Yatagan KSP vs Yatagan kapt, когда я делал замеры).

А есть идеи почему так вышло? Интуитивно KSP должен быть быстрее, так как не надо генерить стабы. Или у вас остались в модуле подключенные kapt зависимости?

Вероятно, скорость "холодной" сборки улучшилась значительно по сравнению с kapt, тут я, к сожалению, не проводил детальных измерений. Скорость инкрементальной сборки, улучшение которой стояло у меня как основная цель, улучшилась на ~5% в различных сценариях по сравнению с kapt, но, ввиду того, что KSP-бэкенд несколько раз разламывался при обновлении версии из-за багов KSP в поддержке Java, мы так и не смогли его хорошенько обкатать и измерить реальный профит. Так же KSP-бэкенд наверное может работать немного медленее, чем мог бы, из-за того, что нужно конвертировать Kotlin-сущности в Java для генерации Java кода. Ситуацию планируем исправить в версии 2.0.0, где задумывается поддержка KMP и генерации кода на Kotlin в KSP - если проект получит интерес. Сейчас я расчитываю получить фидбэк по использованию KSP в текущем виде от pure-Kotlin проектов, так как у нас такого опыта ещё нет.

Я правильно понимаю, что отсутствует генерация фреймворком Builder для компонента?
И теперь обязательно надо создавать вложенный класс с аннотацией Component.Builder у каждого компонента.

И сделано это по всей видимости, чтобы поддержать реализацию с рефлексией.

Dagger, например, сам генерирует Dagger<ComponentName>.Builder если отсутствует костомный Component.Builder.

Просто фича то прикольная была) К пример, если в проекте есть единая точка получения зависимостей, то можно было вообще выкинуть из компонента код с его созданием, оставив только метод create. Ну, а подстановку зависимостей делать под капотом с помощью рефлексии или вот пример с KSP (там в конце).

Было бы круто добавить в будущем, чтобы они генерировались, например, с помощью отдельного процессора. Иначе кажется, что многие в любом случае свою реализацию такого процессора у себя сделают)

Интересный поинт. Да, верно, для рефлексии нужен явный интерфейс, который можно реализовать и который может быть использован в клиентском коде. Не обязательно декларировать явный Component.Builder только тогда, когда компонент корневой и у него нет внешних зависимостей - тогда используется Yatagan.create. Так что, автоматически генерировать такие вещи не получится. Если вам это интересно, то можете предложить решение, которое будет учитывать рефлексийный бэкенд, в github issue и его, вероятно, можно будет реализовать, если оно будет достаточно общим.

А можно узнать, чем это лучше Kodein?

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

Единственное - (кому минус, кому плюс) - только Kotlin, в java не заработает.

Yatagan не лучше чем Kodein, они решают немного разные задачи. Например, если вы только начинаете свой проект, и у вас есть возможность выбрать DI-фреймворк и вы выберете Kodein - то скорее всего останетесь доволны. А если у вас большой проект, который уже давно использует Dagger - то Yatagan может улучшить вам жизнь.

Хорошаяя попытка! Удачи! С моей точки зрения, у проекта есть потенциал.

Подскажите, есть ли возможность постепенного перехода с dagger на Yatagan? Или инструменты для миграции? Применять Yatagan не пробовал, но мне кажется, что для большого проекта это будет на грани невозможного.

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

Спасибо. По опыту миграции больших проектов это вполне реальная задача, и мы успешно мигрировали несколько продуктов у себя внутри. Главное, внимательно прочитать документацию по различиям Yatagan и Dagger, оценить объем правок, необходимых для того, чтобы код заработал на Yatagan. Постепенная миграция возможна в том смысле, что правки можно делать ещё в рамках Dagger - заменить все @Nullable привязки, явно декларировать subcomponent'ы; удостовериться, что все декларации компонентов являются интерфейсами и так далее по доке - это все можно делать постепенно, не ломая совместимости с Dagger. После этого, достаточно будет просто автоматически заменить package-name в импортах и починить оставшиеся проблемы, которых должно быть немного, в рамках финальной миграции. Специальных инструментов миграции пока нет.

если я правильно понял, то сначала сами придумали себе проблем, а потом героически с ними боролись)))

делальщики яндекс.браузера для андроид, когда вы в нём нормально сделаете alert/prompt/confirm от javascript'а? нормально, то есть чтобы они теме соответствовали и читабельными были. писал в поддержку давно, но не шевелится что-то

Sign up to leave a comment.