Einleitung: Was ist Dependency Injection und warum wird sie benötigt?
\nStellen Sie sich vor, Sie bauen ein Haus. Sie brauchen Ziegel, Zement und Werkzeuge. Anstatt jedes Mal selbst zum Lager zu laufen, bestellen Sie einfach die Lieferung aller Materialien zur Baustelle. Dependency Injection (DI) oder Abhängigkeitsinjektion ist das gleiche Prinzip, nur für Ihren Code.
\nDependency Injection ist ein Entwurfsmuster, bei dem ein Objekt seine Abhängigkeiten von außen erhält, anstatt sie selbst zu erstellen. Das macht den Code flexibler, testbarer und leichter wartbar. In diesem Artikel betrachten wir DI am Beispiel von Kotlin – einer modernen Programmiersprache, die sich hervorragend zum Erlernen dieses Musters eignet.
\n\nDas Problem: Warum Code ohne DI spröde wird
\nBetrachten wir ein typisches Beispiel. Angenommen, wir haben eine Klasse UserService, die E-Mail-Benachrichtigungen versendet. Ohne DI sieht sie so aus:
class EmailService {\n fun sendEmail(to: String, message: String) {\n println("Sende E-Mail an $to: $message")\n }\n}\n\nclass UserService {\n private val emailService = EmailService() // Feste Kopplung!\n\n fun notifyUser(email: String) {\n emailService.sendEmail(email, "Hallo, Benutzer!")\n }\n}\nWo liegt das Problem? UserService ist fest an eine konkrete Implementierung von EmailService gebunden. Wenn Sie:
\n- \n
- EmailService durch SmsService ersetzen; \n
- Logging hinzufügen; \n
- Unit-Tests mit einem Mock-Objekt schreiben möchten, \n
— müssen Sie den Code von UserService ändern. Dies verletzt das Open/Closed-Prinzip (Offen/Geschlossen-Prinzip) und erschwert das Testen.
\n\nDie Lösung: Manuelle Abhängigkeitsinjektion in Kotlin
\nDer einfachste Weg, DI zu implementieren, ist die Übergabe der Abhängigkeit über den Konstruktor. In Kotlin geht das sehr prägnant:
\n// 1. Erstellen eines Interfaces (Abstraktion)\ninterface NotificationService {\n fun send(to: String, message: String)\n}\n\n// 2. Implementieren konkreter Dienste\nclass EmailService : NotificationService {\n override fun send(to: String, message: String) {\n println("E-Mail an $to: $message")\n }\n}\n\nclass SmsService : NotificationService {\n override fun send(to: String, message: String) {\n println("SMS an $to: $message")\n }\n}\n\n// 3. Injizieren der Abhängigkeit über den Konstruktor\nclass UserService(private val notificationService: NotificationService) {\n fun notifyUser(email: String) {\n notificationService.send(email, "Hallo!")\n }\n}\n\n// 4. Verwendung\nfun main() {\n val emailService = EmailService()\n val userService = UserService(emailService) // Injektion!\n userService.notifyUser("user@example.com")\n}\nVorteile dieses Ansatzes:
\n- \n
- Flexibilität:
EmailServicekann leicht durchSmsServiceersetzt werden; \n - Testbarkeit: In Tests können Sie ein Mock-Objekt übergeben: \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 == "Hallo!")\n}\n\nDI-Frameworks: Koin und Dagger für Kotlin
\nBei manuellem DI schreibt man in großen Projekten viel Boilerplate-Code. Hier helfen DI-Frameworks. In der Kotlin-Welt sind zwei beliebt: Koin (einfach) und Dagger/Hilt (leistungsstark).
\n\nKoin: Einfachheit und Geschwindigkeit
\nKoin ist ein leichtgewichtiges DI-Framework, das in Kotlin geschrieben ist. Es erfordert keine Codegenerierung und ist