Найти тему

Внедрение зависимостей Kotlin с помощью Kodein

Оглавление

1. Общий обзор

В этой статье мы представим Kodein — чистый фреймворк для внедрения зависимостей Kotlin (DI) — и сравним его с другими популярными фреймворками DI.

2. Зависимость

Во-первых, давайте добавим зависимость от кодеина в наш pom.xml:

<dependency>
<groupId>com.github.salomonbrys.kodein</groupId>
<artifactId>kodein</artifactId>
<version>4.1.0</version>
</dependency>

3. Конфигурация

Мы будем использовать приведенную ниже модель для иллюстрации конфигурации на основе Kodein:

class Controller(private val service : Service)

class Service(private val dao: Dao, private val tag: String)

interface Dao

class JdbcDao : Dao

class MongoDao : Dao

4. Типы привязок

Платформа моделирования предлагает различные типы привязок. Давайте подробнее рассмотрим, как они работают и как их использовать.

4.1. Singleton

При одноэлементной привязке целевой компонент создается лениво при первом доступе и повторно используется при всех последующих запросах:

var created = false;
val kodein = Kodein {
bind<Dao>() with singleton {
created = true
MongoDao()
}
}

assertThat(created).isFalse()

val dao1: Dao = kodein.instance()

assertThat(created).isFalse()

val dao2: Dao = kodein.instance()

assertThat(dao1).isSameAs(dao2)

Примечание: мы можем использовать Kodein.instance() для извлечения компонентов, управляемых целью, на основе типа статической переменной.

4.2. Eager Singleton

Это похоже на привязку к синглтону. Единственное отличие заключается в том, что блок инициализации вызывается автоматически:

var created = false;
val kodein = Kodein {
bind<Dao>() with eagerSingleton {
created = true
MongoDao()
}
}

assertThat(created).isTrue()
val dao1: Dao = kodein.instance()
val dao2: Dao = kodein.instance()

assertThat(dao1).isSameAs(dao2)

4.3. Factory

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

val kodein = Kodein {
bind<Dao>() with singleton { MongoDao() }
bind<Service>() with factory { tag: String -> Service(instance(), tag) }
}
val service1: Service = kodein.with("myTag").instance()
val service2: Service = kodein.with("myTag").instance()

assertThat(service1).isNotSameAs(service2)

Примечание: мы можем использовать Kodein.instance() для настройки транзитивных зависимостей.

4.4. Multiton

Multiton очень похожа на Factory. Единственное отличие заключается в том, что при последующих вызовах возвращается один и тот же объект с одним и тем же аргументом:

val kodein = Kodein {
bind<Dao>() with singleton { MongoDao() }
bind<Service>() with multiton { tag: String -> Service(instance(), tag) }
}
val service1: Service = kodein.with("myTag").instance()
val service2: Service = kodein.with("myTag").instance()

assertThat(service1).isSameAs(service2)

4.5. Provider

Это Factory без каких-либо аргументов:

val kodein = Kodein {
bind<Dao>() with provider { MongoDao() }
}
val dao1: Dao = kodein.instance()
val dao2: Dao = kodein.instance()

assertThat(dao1).isNotSameAs(dao2)

4.6. Instance

Мы можем зарегистрировать предварительно сконфигурированный экземпляр компонента в контейнере:

val dao = MongoDao()
val kodein = Kodein {
bind<Dao>() with instance(dao)
}
val fromContainer: Dao = kodein.instance()

assertThat(dao).isSameAs(fromContainer)

4.7. Tagging

Мы также можем зарегистрировать более одного компонента одного и того же типа под разными тегами:

val kodein = Kodein {
bind<Dao>("dao1") with singleton { MongoDao() }
bind<Dao>("dao2") with singleton { MongoDao() }
}
val dao1: Dao = kodein.instance("dao1")
val dao2: Dao = kodein.instance("dao2")

assertThat(dao1).isNotSameAs(dao2)

4.8. Constant

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

val kodein = Kodein {
constant("magic") with 42
}
val fromContainer: Int = kodein.instance("magic")

assertThat(fromContainer).isEqualTo(42)

5. Разделение привязок

Kodein позволяет нам конфигурировать компоненты в отдельных блоках и объединять их.

5.1. Модули

Мы можем сгруппировать компоненты по определенным критериям — например, все классы, связанные с сохранением данных, — и объединить блоки для создания результирующего контейнера:

val jdbcModule = Kodein.Module {
bind<Dao>() with singleton { JdbcDao() }
}
val kodein = Kodein {
import(jdbcModule)
bind<Controller>() with singleton { Controller(instance()) }
bind<Service>() with singleton { Service(instance(), "myService") }
}

val dao: Dao = kodein.instance()
assertThat(dao).isInstanceOf(JdbcDao::class.java)

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

5.2. Состав

Мы можем расширить один экземпляр Kodein из другого — это позволяет нам повторно использовать beans:

val persistenceContainer = Kodein {
bind<Dao>() with singleton { MongoDao() }
}
val serviceContainer = Kodein {
extend(persistenceContainer)
bind<Service>() with singleton { Service(instance(), "myService") }
}
val fromPersistence: Dao = persistenceContainer.instance()
val fromService: Dao = serviceContainer.instance()

assertThat(fromPersistence).isSameAs(fromService)

5.3. Переопределение

Мы можем переопределить привязки — это может быть полезно для тестирования:

class InMemoryDao : Dao

val commonModule = Kodein.Module {
bind<Dao>() with singleton { MongoDao() }
bind<Service>() with singleton { Service(instance(), "myService") }
}
val testContainer = Kodein {
import(commonModule)
bind<Dao>(overrides = true) with singleton { InMemoryDao() }
}
val dao: Dao = testContainer.instance()

assertThat(dao).isInstanceOf(InMemoryDao::class.java)

6. Множественные привязки

Мы можем сконфигурировать в контейнере более одного компонента с одним и тем же общим (супер-) типом:

val kodein = Kodein {
bind() from setBinding<Dao>()
bind<Dao>().inSet() with singleton { MongoDao() }
bind<Dao>().inSet() with singleton { JdbcDao() }
}
val daos: Set<Dao> = kodein.instance()

assertThat(daos.map {it.javaClass as Class<*>})
.containsOnly(MongoDao::class.java, JdbcDao::class.java)

7. Инжектор

Код нашего приложения не знал о Kodein во всех примерах, которые мы использовали ранее — он использовал обычные аргументы конструктора, которые были предоставлены во время инициализации контейнера.

Однако фреймворк допускает
альтернативный способ настройки зависимостей с помощью делегированных свойств и инжекторов:

class Controller2 {
private val injector = KodeinInjector()
val service: Service by injector.instance()
fun injectDependencies(kodein: Kodein) = injector.inject(kodein)
}
val kodein = Kodein {
bind<Dao>() with singleton { MongoDao() }
bind<Service>() with singleton { Service(instance(), "myService") }
}
val controller = Controller2()
controller.injectDependencies(kodein)

assertThat(controller.service).isNotNull

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

8. Использование Kodein В Android

В Android контейнер Kodein настраивается в пользовательском классе приложения, а затем привязывается к экземпляру контекста. Предполагается, что все компоненты (activities, фрагменты, службы, широковещательные приемники) расширяются из служебных классов, таких как KodeinActivity и KodeinFragment:

class MyActivity : Activity(), KodeinInjected {
override val injector = KodeinInjector()

val random: Random by instance()

override fun onCreate(savedInstanceState: Bundle?) {
inject(appKodein())
}
}

9. Анализ

В этом разделе мы увидим, как Kodein сравнивается с популярными DI-фреймворками.

9.1. Spring Framework

Платформа Spring Framework гораздо более функциональна, чем Kodein. Например, в Spring есть очень удобное средство сканирования компонентов. Когда мы помечаем наши классы определенными аннотациями, такими как @Component, @Service и @Named, при сканировании компонентов эти классы автоматически обнаруживаются во время инициализации контейнера.

Spring также имеет мощные точки расширения для метапрограммирования, BeanPostProcessor и BeanFactoryPostProcessor, которые могут иметь решающее значение при адаптации настроенного приложения к конкретной среде.

Наконец, Spring предоставляет несколько удобных технологий, построенных на его основе, включая AOP, транзакции, фреймворк тестирования и многие другие. Если мы хотим их использовать, стоит придерживаться контейнера Spring IoC.

9.2. Dagger 2

Фреймворк Dagger 2 не так богат функционалом, как Spring Framework, но он популярен в разработке под Android благодаря своей скорости (он генерирует Java-код, который выполняет внедрение и просто выполняет его во время выполнения) и размеру.

Давайте сравним количество методов и размеры библиотек, используя MethodsCount:

Kodein:

-2

Обратите внимание, что большая часть этих чисел связана с зависимостью kotlin-stdlib. Если мы исключим ее, то получим 1282 метода и размер файла DEX в 244 КБАЙТ.

Dagger 2:

-3

Мы видим, что фреймворк Dagger 2 добавляет гораздо меньше методов, а его JAR-файл меньше по размеру.

Что касается использования — это очень похоже в том смысле, что пользовательский код настраивает зависимости (через Injector в Kodein и аннотации JSR-330 в Dagger 2), а затем вводит их с помощью одного вызова метода.

Однако ключевой особенностью Dagger 2 является то, что он
проверяет график зависимостей во время компиляции, поэтому он не позволит приложению скомпилироваться в случае ошибки конфигурации.

10. Заключение

Теперь мы знаем, как использовать Kodein для внедрения зависимостей, какие параметры конфигурации он предоставляет и как он сравнивается с несколькими другими популярными DI-фреймворками. Однако вам решать, использовать ли его в реальных проектах.

Оригинал статьи: https://www.baeldung.com/kotlin/kodein-dependency-injection