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:
Обратите внимание, что большая часть этих чисел связана с зависимостью kotlin-stdlib. Если мы исключим ее, то получим 1282 метода и размер файла DEX в 244 КБАЙТ.
Dagger 2:
Мы видим, что фреймворк Dagger 2 добавляет гораздо меньше методов, а его JAR-файл меньше по размеру.
Что касается использования — это очень похоже в том смысле, что пользовательский код настраивает зависимости (через Injector в Kodein и аннотации JSR-330 в Dagger 2), а затем вводит их с помощью одного вызова метода.
Однако ключевой особенностью Dagger 2 является то, что он проверяет график зависимостей во время компиляции, поэтому он не позволит приложению скомпилироваться в случае ошибки конфигурации.
10. Заключение
Теперь мы знаем, как использовать Kodein для внедрения зависимостей, какие параметры конфигурации он предоставляет и как он сравнивается с несколькими другими популярными DI-фреймворками. Однако вам решать, использовать ли его в реальных проектах.
Оригинал статьи: https://www.baeldung.com/kotlin/kodein-dependency-injection