Injection de dépendance pour Kotlin avec Injekt

Injection de dépendance pour Kotlin avec Injekt

1. introduction

Dependency Injection is a software development pattern where we separate object creation from the objects that are being created. Nous pouvons l'utiliser pour garder le code de notre application principale aussi propre que possible. Cela facilite le travail avec et le test.

Dans ce didacticiel, nous allons explorer le framework Injekt qui apporte l'injection de dépendances à Kotlin.

2. Qu'est-ce que l'injection de dépendance?

L'injection de dépendance est un modèle de développement logiciel commun utilisé pour faciliter la maintenance et la construction d'applications. En utilisant ce modèle, nous séparons la construction de nos objets d’application du comportement d’exécution réel. Cela signifie que chaque partie de notre application est autonome et ne dépend directement d'aucune autre. Au lieu de cela, lorsque nous construisons nos objets, nous pouvons fournir toutes les dépendances dont il a besoin.

En utilisant l'injection de dépendance, nous pouvons facilement tester notre code. Parce que nous contrôlons les dépendances, nous pouvons en fournir différentes au moment du test. Cela permet d'utiliser des objets fictifs ou stub afin que notre code de test contrôle totalement tout ce qui se trouve en dehors de l'unité.

Nous pouvons également modifier de manière triviale les implémentations de certaines parties de l’application sans qu’il soit nécessaire de modifier d’autres. Par exemple, peut remplacer un objet DAO basé sur JPA par un objet basé sur MongoDB, et tant qu'il implémente la même interface, rien d'autre ne doit changer. En effet, la dépendance qui est injectée a changé, mais le code dans lequel elle est injectée ne dépend pas directement d'elle.

En développement Java, le framework d'injection de dépendance le plus connu est Spring. Cependant, lorsque nous utilisons cela, nous apportons de nombreuses fonctionnalités supplémentaires dont nous n'avons souvent pas besoin ni ne voulons. L’injection de dépendance n’a besoin que d’une configuration dans laquelle nous construisons nos objets d’application indépendamment de la façon dont nous les utilisons.

3. Dépendances Maven

Injekt est une bibliothèque Kotlin standard et est disponible sur Maven Central pour être inclus dans notre projet.

Nous pouvons inclure cela, y compris la dépendance suivante dans notre projet:


    uy.kohesive.injekt
    injekt-core
    1.16.1

Pour simplifier le code, il est recommandé d'utiliser des importations d'étoiles pour intégrer Injekt à notre application:

import uy.kohesive.injekt.*
import uy.kohesive.injekt.api.*

4. Câblage d'application simple

Une fois qu'Injekt est disponible, nous pouvons commencer à l'utiliser pour connecter nos classes ensemble afin de construire notre application.

4.1. Démarrer notre application

Dans le cas le plus simple, Injekt fournit une classe de base que nous pouvons utiliser pour notre classe principale d’applications:

class SimpleApplication {
    companion object : InjektMain() {
        @JvmStatic fun main(args: Array) {
            SimpleApplication().run()
        }

        override fun InjektRegistrar.registerInjectables() {
            addSingleton(Server())
        }
    }

    fun run() {
        val server = Injekt.get()
        server.start()
    }
}

Nous pouvons définir nos beans dans la méthoderegisterInjectables, puis la méthoderun est le point d'entrée réel de notre application. Ici, nous pouvons accéder à tous les beans que nous avons enregistrés selon les besoins.

4.2. Présentation des objets Singleton

Comme nous l'avons vu ci-dessus, nous pouvons enregistrer des objets Singleton avec notre application en utilisant la méthodeaddSingleton. Tout cela consiste à créer un objet et à le placer dans notre conteneur d'injection de dépendance pour que d'autres objets puissent y accéder.

Cela signifie également que nous ne pouvons pas référencer d'autres beans dans le conteneur lors de leur création, car le conteneur n'existe pas encore.

Alternativement, nous pouvons enregistrer un rappel pour construire le singleton uniquement lorsque cela est nécessaire.

Cela nous donne la possibilité de dépendre d'autres beans, et cela signifie également que nous ne les créons pas tant que nous n'en avons pas besoin, ce qui nous rend plus efficaces:

class Server(private val config: Config) {
    private val LOG = LoggerFactory.getLogger(Server::class.java)
    fun start() {
        LOG.info("Starting server on ${config.port}")
    }
}
override fun InjektRegistrar.registerInjectables() {
    addSingleton(Config(port = 12345))
    addSingletonFactory { Server(Injekt.get()) }
}

Notez que nous construisons notre beanServer en utilisant une méthode de rappel, et il est fourni avec l'objetConfig requis directement à partir du conteneur Injekt.

Nous n'avons pas besoin d'indiquer à Injekt les types nécessaires ici car il peut les déduire en fonction du contexte - où il doit renvoyer un objet de typeConfig, c'est donc ce que nous obtenons.

4.3. Présentation des objets d'usine

À l'occasion, nous souhaitons créer un nouvel objet à chaque fois qu'il est utilisé. Par exemple, nous pourrions avoir un objet qui est un client réseau pour un autre service, et chaque lieu l’utilisant devrait avoir son client injecté, avec sa connexion réseau et tout le reste.

Nous pouvons y parvenir en utilisant la méthodeaddFactory au lieu deaddSingletonFactory.

La seule différence est que nous allons créer une nouvelle instance à chaque injection, au lieu de la mettre en cache et de la réutiliser:

class Client(private val config: Config) {
    private val LOG = LoggerFactory.getLogger(Client::class.java)
    fun start() {
        LOG.info("Opening connection to on ${config.host}:${config.port}")
    }
}
override fun InjektRegistrar.registerInjectables() {
    addSingleton(Config(host = "example.com", port = 12345))
    addFactory { Client(Injekt.get()) }
}

Dans cet exemple, partout où nous injectons ensuite unClient obtiendra une toute nouvelle instance, mais toutes ces instances partageront le même objetConfig.

5. Accès aux objets

Nous pouvons accéder aux objets construits par le conteneur de différentes manières, en fonction de ce qui convient le mieux. Nous avons déjà vu plus haut que nous pouvions injecter un objet du conteneur dans un autre au moment de la construction.

5.1. Accès direct depuis le conteneur

Nous pouvons appelerInjekt.get de n'importe où dans notre code à tout moment, et cela fera la même chose. Cela signifie que nous pouvons également l'appeler à tout moment depuis notre application en direct pour accéder aux objets du conteneur.

Ceci est particulièrement utile pour les objets d'usine, où nous obtiendrions chaque fois une nouvelle instance au moment de l'exécution au lieu d'être injectée de la même manière au moment de la construction:

class Notifier {
    fun sendMessage(msg: String) {
        val client: Client = Injekt.get()
        client.use {
            client.send(msg)
        }
    }
}

Cela signifie également que nous ne sommes pas limités à l'utilisation de classes pour notre code. Nous pouvons également accéder aux objets à partir du conteneur à l'intérieur de fonctions de niveau supérieur.

5.2. Utiliser comme paramètre par défaut

Kotlin nous permet de spécifier des valeurs par défaut pour les paramètres. Nous pouvons également utiliser Injekt ici pour qu'une valeur soit obtenue à partir du conteneur si aucune valeur alternative n'est fournie.

This can be especially useful for writing unit tests, où le même objet peut être utilisé à la fois dans l'application en direct - obtenant automatiquement les dépendances à partir du conteneur, ou à partir de tests unitaires - où nous pouvons fournir une alternative à des fins de test:

class Client(private val config: Config = Injekt.get()) {
    ...
}

Nous pouvons également l'utiliser pour les paramètres de constructeur et les paramètres de méthode, ainsi que pour les classes et les fonctions de niveau supérieur.

5.3. Utilisation de délégués

Injekt nous fournit des délégués que nous pouvons utiliser pour accéder automatiquement aux objets conteneur en tant que champs de classe.

Le déléguéinjectValue obtiendra un objet du conteneur immédiatement lors de la construction de la classe, tandis que le déléguéinjectLazy obtiendra un objet du conteneur uniquement lors de sa première utilisation:

class Notifier {
    private val client: Client by injectLazy()
}

6. Construction d'objets avancés

Jusqu'à présent, nous pouvons réaliser tout ce que nous avons fait sans utiliser Injekt, bien que cela ne soit pas aussi net que lorsque vous utilisiez Injekt.

Nous disposons cependant d’outils de construction plus avancés, permettant des techniques plus difficiles à gérer par nous-mêmes.

6.1. Objets par thread

Once we start accessing objects from the container directly in our code, we start to run the risk of object contention. Nous pouvons résoudre ce problème en obtenant une nouvelle instance à chaque fois - créée avecaddFactory - mais cela peut coûter cher.

Injekt peut également créer une nouvelle instance pour chaque thread qui l'appelle, mais mettre en cache l'instance de ce thread.

Cela évite le risque de conflit - chaque thread ne peut faire qu'une chose à la fois - mais réduit également le nombre d'objets que nous devons créer:

override fun InjektRegistrar.registerInjectables() {
    addPerThreadFactory { Client(Injekt.get()) }
}

Nous pouvons maintenant obtenir un objetClient à tout moment, et ce sera toujours le même pour le thread courant, mais jamais le même que dans n'importe quel autre thread.

6.2. Objets clés

We need to be careful not to get carried away with the per-thread allocation of objects. C'est très bien s'il y a un nombre fixe de threads, mais si des threads sont créés et souvent détruits, alors notre collection d'objets peut augmenter inutilement.

De plus, nous avons parfois besoin d’avoir accès simultanément à différentes instances de la même classe, pour différentes raisons. Nous voulons toujours pouvoir accéder à la même instance pour la même raison.

Injekt nous donne la possibilité d'accéder aux objets d'une collection à clé, lorsque l'appelant qui demande l'objet fournit la clé.

Cela signifie que chaque fois que nous utilisons la même clé, nous obtiendrons le même objet. La méthode d'usine a également accès à cette clé au cas où elle serait nécessaire pour modifier les fonctionnalités d'une manière ou d'une autre:

override fun InjektRegistrar.registerInjectables() {
    addPerKeyFactory { provider: String ->
        OAuthClientDetails(
            clientId = System.getProperty("oauth.provider.${provider}.clientId"),
            clientSecret = System.getProperty("oauth.provider.${provider}.clientSecret")
        )
    }
}

Nous pouvons maintenant obtenir les détails du client OAuth pour un fournisseur nommé - par exemple. “Google” ou “twitter”. L'objet renvoyé est correctement renseigné en fonction des propriétés système définies dans l'application.

7. Construction d'application modulaire

Jusqu'à présent, nous n'avons construit notre conteneur que dans un seul endroit. Cela fonctionne mais deviendra lourd avec le temps.

Injekt gives us the ability to do better than this but splitting our configuration up into modules. Cela nous permet d'avoir des zones de configuration plus petites et plus ciblées. Cela nous permet également d’avoir la configuration incluse dans les bibliothèques auxquelles elles s’appliquent.

Par exemple, nous pourrions avoir une dépendance qui représente un bot Twitter. Cela pourrait inclure un module Injekt afin que toute personne l’utilisant puisse le brancher directement.

Les modules sont des objets Kotlin qui étendent la classe de baseInjektModule et qui implémentent la méthoderegisterInjectables().

Nous l'avons déjà fait avec la classeInjektMain que nous avons utilisée précédemment. C'est une sous-classe directe deInjektModule et fonctionne de la même manière:

object TwitterBotModule : InjektModule {
    override fun InjektRegistrar.registerInjectables() {
        addSingletonFactory { TwitterConfig(clientId = "someClientId", clientSecret = "someClientSecret") }
        addSingletonFactory { config = TwitterBot(Injekt.get()) }
    }
}

Une fois que nous avons un module, nous pouvons l'inclure n'importe où ailleurs dans notre conteneur en utilisant la méthodeimportModule:

override fun InjektRegistrar.registerInjectables() {
    importModule(TwitterBotModule)
}

À ce stade, tous les objets définis dans ce module sont disponibles exactement comme s'ils avaient été définis directement ici.

8. Conclusion

Dans cet article, nous avons présenté une introduction à l'injection de dépendances dans Kotlin, et comment la bibliothèque Injekt rend cela simple à réaliser.

Il y a beaucoup plus de choses que nous pouvons réaliser avec Injekt par rapport à celles présentées ici. Espérons que cela devrait vous aider à vous lancer dans la simple injection de dépendance.

Et, comme toujours, consultez les exemples de toutes ces fonctionnalitésover on GitHub.