Introduction : qu'est-ce que l'Injection de Dépendances et pourquoi en a-t-on besoin ?
\nImaginez que vous construisez une maison. Vous avez besoin de briques, de ciment et d'outils. Au lieu d'aller chaque fois au dépôt pour les chercher, vous commandez simplement la livraison de tous les matériaux sur le chantier. L'Injection de Dépendances (DI) ou injection de dépendances est le même principe, mais pour votre code.
\nL'Injection de Dépendances est un modèle de conception dans lequel un objet reçoit ses dépendances de l'extérieur, plutôt que de les créer lui-même. Cela rend le code plus flexible, testable et facile à maintenir. Dans cet article, nous allons analyser la DI avec l'exemple de Kotlin, un langage de programmation moderne qui se prête parfaitement à l'apprentissage de ce modèle.
\n\nLe problème : pourquoi sans DI le code devient fragile ?
\nPrenons un exemple typique. Supposons que nous ayons une classe UserService qui envoie des notifications par email. Sans DI, elle ressemble à ceci :
class EmailService {\n fun sendEmail(to: String, message: String) {\n println("Envoi d'email à $to : $message")\n }\n}\n\nclass UserService {\n private val emailService = EmailService() // Couplage fort !\n\n fun notifyUser(email: String) {\n emailService.sendEmail(email, "Bonjour, utilisateur !")\n }\n}\nQuel est le problème ? UserService est fortement couplé à une implémentation spécifique de EmailService. Si vous voulez :
\n- \n
- Remplacer EmailService par SmsService ; \n
- Ajouter une journalisation ; \n
- Écrire des tests unitaires avec un objet mock, \n
— vous devrez modifier le code de UserService. Cela viole le principe Open/Closed (ouvert/fermé) et complique les tests.
\n\nSolution : injection manuelle des dépendances en Kotlin
\nLa façon la plus simple d'implémenter la DI est de passer la dépendance via le constructeur. En Kotlin, cela se fait de manière très concise :
\n// 1. Créer une interface (abstraction)\ninterface NotificationService {\n fun send(to: String, message: String)\n}\n\n// 2. Implémenter les services concrets\nclass EmailService : NotificationService {\n override fun send(to: String, message: String) {\n println("Email à $to : $message")\n }\n}\n\nclass SmsService : NotificationService {\n override fun send(to: String, message: String) {\n println("SMS à $to : $message")\n }\n}\n\n// 3. Injecter la dépendance via le constructeur\nclass UserService(private val notificationService: NotificationService) {\n fun notifyUser(email: String) {\n notificationService.send(email, "Bonjour !")\n }\n}\n\n// 4. Utilisation\nfun main() {\n val emailService = EmailService()\n val userService = UserService(emailService) // Injection !\n userService.notifyUser("user@example.com")\n}\nAvantages de cette approche :
\n- \n
- Flexibilité : on peut facilement remplacer
EmailServiceparSmsService; \n - Testabilité : dans les tests, vous pouvez passer un objet mock : \n
class MockNotificationService : NotificationService {\n var lastMessage: String? = null\n override fun send(to: String, message: String) {\n lastMessage = message\n }\n}\n\nfun test() {\n val mock = MockNotificationService()\n val userService = UserService(mock)\n userService.notifyUser("test@test.com")\n assert(mock.lastMessage == "Bonjour !")\n}\n\nFrameworks DI : Koin et Dagger pour Kotlin
\nAvec la DI manuelle, si le projet est volumineux, vous écrivez beaucoup de code passe-partout. C'est là que les frameworks DI viennent à la rescousse. Dans le monde Kotlin, deux sont populaires : Koin (simple) et Dagger/Hilt (puissant).
\n\nKoin : simplicité et rapidité
\nKoin est un framework DI léger, écrit en Kotlin. Il ne nécessite pas de génération de code et est