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:Notez 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.
Nous 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.