Iniezione di dipendenza di Kotlin con Kodein

1. Panoramica

In questo articolo, introdurremo Kodein, un framework di iniezione di dipendenza (DI) Kotlin puro, e lo confronteremo con altri framework DI popolari.

2. Dipendenza

Per prima cosa, aggiungiamo la dipendenza Kodein al nostro pom.xml :

 com.github.salomonbrys.kodein kodein 4.1.0 

Si noti che l'ultima versione disponibile è disponibile su Maven Central o jCenter.

3. Configurazione

Useremo il modello seguente per illustrare la configurazione basata su 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. Tipi di rilegatura

Il framework Kodein offre vari tipi di binding. Diamo un'occhiata più da vicino a come funzionano e come usarli.

4.1. Singleton

Con il binding Singleton , un bean di destinazione viene istanziato pigramente al primo accesso e riutilizzato su tutte le ulteriori richieste:

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)

Nota: possiamo usare Kodein.instance () per recuperare i bean gestiti dalla destinazione in base a un tipo di variabile statica.

4.2. Desideroso Singleton

Questo è simile all'associazione Singleton . L'unica differenza è che il blocco di inizializzazione viene chiamato con impazienza :

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

Con il binding di fabbrica , il blocco di inizializzazione riceve un argomento e ogni volta viene restituito un nuovo oggetto :

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)

Nota: possiamo usare Kodein.instance () per configurare le dipendenze transitive.

4.4. Multiton

L' associazione Multiton è molto simile all'associazione Factory . L'unica differenza è che lo stesso oggetto viene restituito per lo stesso argomento nelle chiamate successive :

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

Questo è un binding Factory senza argomenti :

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

4.6. Esempio

Possiamo registrare un'istanza di bean preconfigurata nel contenitore:

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

4.7. Etichettatura

Possiamo anche registrare più di un bean dello stesso tipo con tag diversi:

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

Questo è zucchero sintattico sull'associazione con tag e si presume che venga utilizzato per le costanti di configurazione - tipi semplici senza ereditarietà:

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

5. Separazione degli attacchi

Kodein ci permette di configurare i bean in blocchi separati e combinarli.

5.1. Moduli

Possiamo raggruppare i componenti in base a criteri particolari - ad esempio, tutte le classi relative alla persistenza dei dati - e combinare i blocchi per costruire un contenitore risultante :

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)

Nota: poiché i moduli contengono regole di associazione, i bean di destinazione vengono ricreati quando lo stesso modulo viene utilizzato in più istanze di Kodein.

5.2. Composizione

Possiamo estendere un'istanza di Kodein da un'altra - questo ci consente di riutilizzare i bean:

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

Possiamo sovrascrivere le associazioni - questo può essere utile per i test:

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. Multi-binding

Possiamo configurare più di un bean con lo stesso tipo (super) comune nel contenitore:

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

Il codice della nostra applicazione non era a conoscenza di Kodein in tutti gli esempi che abbiamo usato prima: utilizzava i normali argomenti del costruttore forniti durante l'inizializzazione del contenitore.

Tuttavia, il framework consente un modo alternativo per configurare le dipendenze tramite proprietà delegate e iniettori :

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.

Come sempre, il codice sorgente per gli esempi sopra può essere trovato su GitHub.