Инъекция зависимостей Kotlin с Kodein

Инъекция зависимости 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:image Обратите внимание, что зависимостьkotlin-stdlib составляет большую часть этих чисел. Когда мы исключаем его, мы получаем 1282 метода и размер DEX 244 КБ.

 

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

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

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

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

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

Как всегда, исходный код для приведенных выше примеров можно найти вover on GitHub.