Kotlin Dependency Injection avec Kodein

Kotlin Dependency Injection avec Kodein

1. Vue d'ensemble

Dans cet article, nous allons présenterKodein - un framework d'injection de dépendances (DI) pur Kotlin - et le comparer avec d'autres frameworks de DI populaires.

2. Dépendance

Tout d'abord, ajoutons la dépendance Kodein à nospom.xml:


    com.github.salomonbrys.kodein
    kodein
    4.1.0

Veuillez noter que la dernière version disponible est disponible surMaven Central oujCenter.

3. Configuration

Nous utiliserons le modèle ci-dessous pour illustrer la configuration basée sur 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. Types de reliure

Le framework Kodein propose différents types de liaisons. Voyons de plus près comment ils fonctionnent et comment les utiliser.

4.1. Singleton

Avec la liaisonSingleton,a target bean is instantiated lazily on the first access et réutilisé sur toutes les demandes ultérieures:

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)

Remarque: nous pouvons utiliserKodein.instance() pour récupérer les beans gérés par la cible en fonction d'un type de variable statique.

4.2. Singleton avide

Ceci est similaire à la liaison deSingleton. La seule différence est quethe 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. Usine

Avec la liaisonFactory, le bloc d'initialisation reçoit un argument, eta 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)

Remarque: nous pouvons utiliserKodein.instance() pour configurer les dépendances transitives.

4.4. Multiton

La liaison deMultiton est très similaire à la liaison deFactory. La seule différence est quethe 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. Fournisseur

Ceci est une liaison sans argumentFactory:

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

assertThat(dao1).isNotSameAs(dao2)

4.6. Exemple

On peutregister a pre-configured bean instance dans le conteneur:

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

assertThat(dao).isSameAs(fromContainer)

4.7. Balisage

On peut aussiregister more than one bean of the same type sous différentes balises:

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. Constant

Il s'agit d'un sucre syntaxique sur une liaison étiquetée et est supposéto be used for configuration constants - types simples sans héritage:

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

assertThat(fromContainer).isEqualTo(42)

5. Séparation des liaisons

Kodein nous permet de configurer les beans dans des blocs séparés et de les combiner.

5.1. Modules

On peutgroup components by particular criteria - par exemple, toutes les classes liées à la persistance des données -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)

Remarque: comme les modules contiennent des règles de liaison, les beans cibles sont recréés lorsque le même module est utilisé dans plusieurs instances Kodein.

5.2. Composition

Nous pouvons étendre une instance Kodein d’une autre, ce qui nous permet de réutiliser des beans:

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. Primordial

Nous pouvons remplacer les liaisons - cela peut être utile pour tester:

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. Liaisons multiples

Nous pouvons configurermore than one bean with the same common (super-)type dans le conteneur:

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. Injecteur

Le code de notre application ne connaissait pas Kodein dans tous les exemples que nous avons utilisés auparavant - il utilisait des arguments de constructeur réguliers fournis lors de l'initialisation du conteneur.

Cependant, le framework autorisean 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

En d'autres termes, une classe de domaine définit des dépendances via un injecteur et les extrait d'un conteneur donné. Such an approach is useful in specific environments like Android.

8. Utiliser Kodein avec Android

In Android, the Kodein container is configured in a custom Application class, and later on, it is bound to the Context instance. Tous les composants (activités, fragments, services, récepteurs de diffusion) sont supposés être étendus à partir des classes d'utilité telles queKodeinActivity etKodeinFragment:

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

    val random: Random by instance()

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

9. Une analyse

Dans cette section, nous verrons comment Kodein se compare aux frameworks de DI populaires.

9.1. Cadre de printemps

Le framework Spring est beaucoup plus riche en fonctionnalités que Kodein. Par exemple, Spring a un trèsconvenient component-scanning facility. Lorsque nous marquons nos classes avec des annotations particulières telles que@Component,@Service et@Named,, l'analyse des composants récupère ces classes automatiquement lors de l'initialisation du conteneur.

Spring a égalementpowerful meta-programming extension points,BeanPostProcessor etBeanFactoryPostProcessor, ce qui peut être crucial lors de l'adaptation d'une application configurée à un environnement particulier.

Enfin, Spring fournit desconvenient technologies built on top of it, notamment AOP, Transactions, Test Framework et bien d'autres. Si nous voulons les utiliser, il vaut la peine de s'en tenir au conteneur Spring IoC.

9.2. Dague 2

Le framework Dagger 2 estnot as feature-rich as Spring Framework, but it’s popular in Android development en raison de sa vitesse (il génère du code Java qui effectue l'injection et ne l'exécute qu'à l'exécution) et de sa taille.

Comparons le nombre et la taille des méthodes des bibliothèques:

Kodein:imageNotez que la dépendancekotlin-stdlib représente la majeure partie de ces nombres. Lorsque nous l'excluons, nous obtenons 1282 méthodes et une taille DEX de 244 Ko.

 

imageNous pouvons voir que le framework Dagger 2 ajoute beaucoup moins de méthodes et que son fichier JAR est plus petit.

En ce qui concerne l'utilisation - c'est très similaire en ce que le code utilisateur configure les dépendances (via les annotationsInjector dans Kodein etJSR-330 dans Dagger 2) et les injecte plus tard via un seul appel de méthode.

Cependant, une caractéristique clé de Dagger 2 est qu'ilvalidates the dependency graph at compile time, donc il ne permettra pas à l'application de se compiler s'il y a une erreur de configuration.

10. Conclusion

Nous savons maintenant comment utiliser Kodein pour l'injection de dépendances, quelles options de configuration il fournit et comment il se compare à quelques autres frameworks DI populaires. Cependant, c'est à vous de décider de l'utiliser ou non dans de vrais projets.

Comme toujours, le code source des exemples ci-dessus peut être trouvéover on GitHub.