Инъекция зависимости Kotlin с Kodein
1. обзор
В этой статье мы познакомимся сKodein - чистой структурой внедрения зависимостей (DI) на Kotlin - и сравним ее с другими популярными платформами DI.
2. зависимость
Во-первых, давайте добавим зависимость Kodein к нашемуpom.xml:
com.github.salomonbrys.kodein
kodein
4.1.0
Обратите внимание, что последняя доступная версия доступна наMaven Central илиjCenter.
3. конфигурация
Мы будем использовать модель ниже для иллюстрации конфигурации на основе кодеина:
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. Типы привязки
Каркас Kodein предлагает различные виды связывания. Давайте подробнее рассмотрим, как они работают и как их использовать.
4.1. одиночка
С привязкойSingleton,a target bean is instantiated lazily on the first access и повторно используется во всех дальнейших запросах:
var created = false;
val kodein = Kodein {
bind() with singleton { MongoDao() }
}
assertThat(created).isFalse()
val dao1: Dao = kodein.instance()
assertThat(created).isFalse()
val dao2: Dao = kodein.instance()
assertThat(dao1).isSameAs(dao2)
Примечание: мы можем использоватьKodein.instance() для получения bean-компонентов, управляемых целевым объектом, на основе типа статической переменной.
4.2. Нетерпеливый синглтон
Это похоже на привязкуSingleton. Единственная разница в том, чтоthe initialization block is called eagerly:
var created = false;
val kodein = Kodein {
bind() with singleton { MongoDao() }
}
assertThat(created).isTrue()
val dao1: Dao = kodein.instance()
val dao2: Dao = kodein.instance()
assertThat(dao1).isSameAs(dao2)
4.3. завод
При привязкеFactory блок инициализации получает аргумент, иa new object is returned from it every time:
val kodein = Kodein {
bind() with singleton { MongoDao() }
bind() 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 очень похоже на связываниеFactory. Единственная разница в том, чтоthe same object is returned for the same argument in subsequent calls:
val kodein = Kodein {
bind() with singleton { MongoDao() }
bind() 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. поставщик
Это привязка без аргументаFactory:
val kodein = Kodein {
bind() with provider { MongoDao() }
}
val dao1: Dao = kodein.instance()
val dao2: Dao = kodein.instance()
assertThat(dao1).isNotSameAs(dao2)
4.6. Экземпляр
Мы можемregister a pre-configured bean instance в контейнере:
val dao = MongoDao()
val kodein = Kodein {
bind() with instance(dao)
}
val fromContainer: Dao = kodein.instance()
assertThat(dao).isSameAs(fromContainer)
4.7. Tagging
Мы также можем использоватьregister more than one bean of the same type в разных тегах:
val kodein = Kodein {
bind("dao1") with singleton { MongoDao() }
bind("dao2") with singleton { MongoDao() }
}
val dao1: Dao = kodein.instance("dao1")
val dao2: Dao = kodein.instance("dao2")
assertThat(dao1).isNotSameAs(dao2)
4.8. постоянная
Это синтаксический сахар по сравнению с привязкой к тегам и предполагается, чтоto be used for configuration constants - простые типы без наследования:
val kodein = Kodein {
constant("magic") with 42
}
val fromContainer: Int = kodein.instance("magic")
assertThat(fromContainer).isEqualTo(42)
5. Разделение привязок
Кодеин позволяет нам конфигурировать компоненты в отдельных блоках и объединять их.
5.1. Модули
Мы можемgroup components by particular criteria - например, все классы, связанные с сохранением данных -and combine the blocks to build a resulting container:
val jdbcModule = Kodein.Module {
bind() with singleton { JdbcDao() }
}
val kodein = Kodein {
import(jdbcModule)
bind() with singleton { Controller(instance()) }
bind() with singleton { Service(instance(), "myService") }
}
val dao: Dao = kodein.instance()
assertThat(dao).isInstanceOf(JdbcDao::class.java)
Примечание: поскольку модули содержат правила привязки, целевые bean-компоненты воссоздаются, когда один и тот же модуль используется в нескольких экземплярах Kodein.
5.2. Состав
Мы можем расширить один экземпляр Kodein от другого - это позволяет нам повторно использовать bean-компоненты:
val persistenceContainer = Kodein {
bind() with singleton { MongoDao() }
}
val serviceContainer = Kodein {
extend(persistenceContainer)
bind() 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() with singleton { MongoDao() }
bind() with singleton { Service(instance(), "myService") }
}
val testContainer = Kodein {
import(commonModule)
bind(overrides = true) with singleton { InMemoryDao() }
}
val dao: Dao = testContainer.instance()
assertThat(dao).isInstanceOf(InMemoryDao::class.java)
6. Мульти-привязки
Мы можем настроитьmore than one bean with the same common (super-)type в контейнере:
val kodein = Kodein {
bind() from setBinding()
bind().inSet() with singleton { MongoDao() }
bind().inSet() with singleton { JdbcDao() }
}
val daos: Set = kodein.instance()
assertThat(daos.map {it.javaClass as Class<*>})
.containsOnly(MongoDao::class.java, JdbcDao::class.java)
7. форсунка
Код нашего приложения не знал о Kodein во всех примерах, которые мы использовали ранее - он использовал обычные аргументы конструктора, которые были предоставлены во время инициализации контейнера.
Однако фреймворк позволяетan alternative way to configure dependencies through delegated properties and Injectors:
class Controller2 {
private val injector = KodeinInjector()
val service: Service by injector.instance()
fun injectDependencies(kodein: Kodein) = injector.inject(kodein)
}
val kodein = Kodein {
bind() with singleton { MongoDao() }
bind() with singleton { Service(instance(), "myService") }
}
val controller = Controller2()
controller.injectDependencies(kodein)
assertThat(controller.service).isNotNull
Другими словами, класс домена определяет зависимости через инжектор и извлекает их из данного контейнера. Such an approach is useful in specific environments like Android.
8. Использование Kodein с Android
In Android, the Kodein container is configured in a custom Application class, and later on, it is bound to the Context instance. Предполагается, что все компоненты (действия, фрагменты, службы, широковещательные приемники) являются расширенными из служебных классов, таких как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 оченьconvenient component-scanning facility. Когда мы отмечаем наши классы определенными аннотациями, такими как@Component,@Service и@Named,, сканирование компонентов автоматически выбирает эти классы во время инициализации контейнера.
Spring также имеетpowerful meta-programming extension points,BeanPostProcessor иBeanFactoryPostProcessor, которые могут иметь решающее значение при адаптации настроенного приложения к конкретной среде.
Наконец, Spring предоставляет несколькоconvenient technologies built on top of it, включая AOP, Transactions, Test Framework и многие другие. Если мы хотим использовать их, стоит придерживаться контейнера Spring IoC.
9.2. Кинжал 2
Фреймворк Dagger 2 -not as feature-rich as Spring Framework, but it’s popular in Android development из-за его скорости (он генерирует Java-код, который выполняет инъекцию и просто выполняет его во время выполнения) и размера.
Давайте сравним количество и размеры методов в библиотеках:
Kodein: Обратите внимание, что зависимостьkotlin-stdlib составляет большую часть этих чисел. Когда мы исключаем его, мы получаем 1282 метода и размер DEX 244 КБ.
Что касается использования - он очень похож в том, что пользовательский код настраивает зависимости (черезInjector в Kodein и аннотацииJSR-330 в Dagger 2), а затем внедряет их с помощью одного вызова метода.
Однако ключевой особенностью Dagger 2 является то, что онvalidates the dependency graph at compile time, поэтому он не позволит приложению компилироваться в случае ошибки конфигурации.
10. Заключение
Теперь мы знаем, как использовать Kodein для внедрения зависимостей, какие параметры конфигурации он предоставляет и как он сравнивается с парой других популярных DI-сред. Однако вам решать, использовать ли его в реальных проектах.
Как всегда, исходный код для приведенных выше примеров можно найти вover on GitHub.