Kotlin-Abhängigkeitsinjektion mit Kodein

1. Übersicht

In diesem Artikel stellen wir Kodein vor - ein reines Kotlin-Abhängigkeitsinjektions-Framework (DI) - und vergleichen es mit anderen gängigen DI-Frameworks.

2. Abhängigkeit

Fügen wir zunächst die Kodein-Abhängigkeit zu unserer pom.xml hinzu :

 com.github.salomonbrys.kodein kodein 4.1.0 

Bitte beachten Sie, dass die neueste verfügbare Version entweder in Maven Central oder in jCenter verfügbar ist.

3. Konfiguration

Wir werden das folgende Modell verwenden, um die Kodein-basierte Konfiguration zu veranschaulichen:

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

Das Kodein-Framework bietet verschiedene Bindungstypen. Schauen wir uns genauer an, wie sie funktionieren und wie sie verwendet werden.

4.1. Singleton

Bei der Singleton- Bindung wird eine Ziel-Bean beim ersten Zugriff träge instanziiert und bei allen weiteren Anforderungen wiederverwendet:

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)

Hinweis: Wir können Kodein.instance () verwenden, um zielverwaltete Beans basierend auf einem statischen Variablentyp abzurufen.

4.2. Eifriger Singleton

Dies ähnelt der Singleton- Bindung. Der einzige Unterschied besteht darin, dass der Initialisierungsblock eifrig aufgerufen wird :

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

Bei der Factory- Bindung erhält der Initialisierungsblock ein Argument, und jedes Mal wird ein neues Objekt von ihm zurückgegeben :

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)

Hinweis: Wir können Kodein.instance () zum Konfigurieren von transitiven Abhängigkeiten verwenden.

4.4. Multiton

Die Multitonenbindung ist der Factory- Bindung sehr ähnlich . Der einzige Unterschied besteht darin, dass in nachfolgenden Aufrufen dasselbe Objekt für dasselbe Argument zurückgegeben wird :

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

Dies ist eine No-Arg- Factory- Bindung:

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

4.6. Beispiel

Wir können eine vorkonfigurierte Bean-Instanz im Container registrieren :

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

4.7. Markieren

Wir können auch mehr als eine Bean des gleichen Typs unter verschiedenen Tags registrieren :

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

Dies ist syntaktischer Zucker über markierte Bindung und wird vermutlich für Konfigurationskonstanten verwendet - einfache Typen ohne Vererbung:

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

5. Trennung der Bindungen

Mit Kodein können wir Beans in separaten Blöcken konfigurieren und kombinieren.

5.1. Module

Wir können Komponenten nach bestimmten Kriterien gruppieren - zum Beispiel nach allen Klassen, die sich auf die Datenpersistenz beziehen - und die Blöcke kombinieren, um einen resultierenden Container zu erstellen :

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)

Hinweis: Da Module Bindungsregeln enthalten, werden Ziel-Beans neu erstellt, wenn dasselbe Modul in mehreren Kodein-Instanzen verwendet wird.

5.2. Komposition

Wir können eine Kodein-Instanz von einer anderen erweitern - so können wir Beans wiederverwenden:

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. Überschreiben

Wir können Bindungen überschreiben - dies kann zum Testen nützlich sein:

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

Wir können mehr als eine Bean mit demselben gemeinsamen (Super-) Typ im Container konfigurieren :

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

Unser Anwendungscode kannte Kodein in allen zuvor verwendeten Beispielen nicht - er verwendete reguläre Konstruktorargumente, die während der Initialisierung des Containers bereitgestellt wurden.

Das Framework bietet jedoch eine alternative Möglichkeit zum Konfigurieren von Abhängigkeiten über delegierte Eigenschaften und Injektoren :

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

In other words, a domain class defines dependencies through an injector and retrieves them from a given container. Such an approach is useful in specific environments like Android.

8. Using Kodein With Android

In Android, the Kodein container is configured in a custom Application class, and later on, it is bound to the Context instance. All components (activities, fragments, services, broadcast receivers) are assumed to be extended from the utility classes like KodeinActivity and KodeinFragment:

class MyActivity : Activity(), KodeinInjected { override val injector = KodeinInjector() val random: Random by instance() override fun onCreate(savedInstanceState: Bundle?) { inject(appKodein()) } }

9. Analysis

In this section, we'll see how Kodein compares with popular DI frameworks.

9.1. Spring Framework

The Spring Framework is much more feature-rich than Kodein. For example, Spring has a very convenient component-scanning facility. When we mark our classes with particular annotations like @Component, @Service, and @Named, the component scan picks up those classes automatically during container initialization.

Spring also has powerful meta-programming extension points, BeanPostProcessor and BeanFactoryPostProcessor, which might be crucial when adapting a configured application to a particular environment.

Finally, Spring provides some convenient technologies built on top of it, including AOP, Transactions, Test Framework, and many others. If we want to use these, it's worth sticking with the Spring IoC container.

9.2. Dagger 2

The Dagger 2 framework is not as feature-rich as Spring Framework, but it's popular in Android development due to its speed (it generates Java code which performs the injection and just executes it in runtime) and size.

Let's compare the libraries' method counts and sizes:

Kodein:Note that the kotlin-stdlib dependency accounts for the bulk of these numbers. When we exclude it, we get 1282 methods and 244 KB DEX size.

Dagger 2:

We can see that the Dagger 2 framework adds far fewer methods and its JAR file is smaller.

Regarding the usage — it's very similar in that the user code configures dependencies (through Injector in Kodein and JSR-330 annotations in Dagger 2) and later on injects them through a single method call.

However, a key feature of Dagger 2 is that it validates the dependency graph at compile time, so it won't allow the application to compile if there is a configuration error.

10. Conclusion

We now know how to use Kodein for dependency injection, what configuration options it provides, and how it compares with a couple of other popular DI frameworks. However, it's up to you to decide whether to use it in real projects.

Wie immer finden Sie den Quellcode für die obigen Beispiele auf GitHub.