Kotlin Dependency Injection dengan Kodein

1. Ikhtisar

Pada artikel ini, kami akan memperkenalkan Kodein - kerangka kerja injeksi ketergantungan Kotlin murni (DI) - dan membandingkannya dengan kerangka kerja DI populer lainnya.

2. Ketergantungan

Pertama, mari tambahkan ketergantungan Kodein ke pom.xml kita :

 com.github.salomonbrys.kodein kodein 4.1.0 

Harap dicatat bahwa versi terbaru yang tersedia tersedia di Maven Central atau jCenter.

3. Konfigurasi

Kami akan menggunakan model di bawah ini untuk mengilustrasikan konfigurasi berbasis 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. Jenis Pengikatan

Kerangka Kodein menawarkan berbagai jenis pengikatan. Mari kita lihat lebih dekat cara kerjanya dan cara menggunakannya.

4.1. Singleton

Dengan pengikatan Singleton , kacang target dibuat secara malas pada akses pertama dan digunakan kembali pada semua permintaan selanjutnya:

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)

Catatan: kita bisa menggunakan Kodein.instance () untuk mengambil kacang yang dikelola target berdasarkan tipe variabel statis.

4.2. Singleton yang bersemangat

Ini mirip dengan pengikatan Singleton . Satu-satunya perbedaan adalah blok inisialisasi dipanggil dengan penuh semangat :

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

Dengan pengikatan Pabrik , blok inisialisasi menerima argumen, dan objek baru dikembalikan darinya setiap kali :

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)

Catatan: kita bisa menggunakan Kodein.instance () untuk mengonfigurasi dependensi transitif.

4.4. Multiton

Penjilidan multiton sangat mirip dengan penjilidan Pabrik . Satu-satunya perbedaan adalah bahwa objek yang sama dikembalikan untuk argumen yang sama dalam panggilan berikutnya :

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

Ini adalah pengikatan Pabrik tanpa argumen :

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

4.6. Contoh

Kita dapat mendaftarkan instance kacang yang telah dikonfigurasi sebelumnya di dalam wadah:

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

4.7. Pemberian tag

Kami juga dapat mendaftarkan lebih dari satu kacang dengan jenis yang sama di bawah tag yang berbeda:

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

Ini adalah gula sintaksis di atas pengikatan tag dan diasumsikan digunakan untuk konstanta konfigurasi - tipe sederhana tanpa pewarisan:

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

5. Pemisahan Bindings

Kodein memungkinkan kita untuk mengkonfigurasi kacang dalam blok terpisah dan menggabungkannya.

5.1. Modul

Kita dapat mengelompokkan komponen berdasarkan kriteria tertentu - misalnya, semua kelas yang terkait dengan persistensi data - dan menggabungkan blok untuk membangun wadah yang dihasilkan :

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)

Catatan: karena modul berisi aturan pengikatan, kacang target dibuat ulang ketika modul yang sama digunakan dalam beberapa contoh Kodein.

5.2. Komposisi

Kita dapat memperluas satu contoh Kodein dari yang lain - ini memungkinkan kita untuk menggunakan kembali kacang:

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

Kita dapat mengganti binding - ini berguna untuk pengujian:

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-Bindings

Kita dapat mengkonfigurasi lebih dari satu kacang dengan tipe umum (super-) yang sama dalam wadah:

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

Kode aplikasi kami tidak mengetahui Kodein di semua contoh yang kami gunakan sebelumnya - ini menggunakan argumen konstruktor biasa yang disediakan selama inisialisasi penampung.

Namun, kerangka kerja memungkinkan cara alternatif untuk mengonfigurasi dependensi melalui properti yang didelegasikan dan Injector :

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.

Seperti biasa, kode sumber untuk contoh di atas dapat ditemukan di GitHub.