Dependency Injection (DI) adalah teknik menyediakan dependensi untuk sebuah object dari luar, alih-alih object tersebut membuat dependensinya sendiri. Teknik ini melengkapi prinsip Dependency Inversion dengan memberi mekanisme konkret bagaimana object menerima implementasi interface yang dibutuhkan. Pada chapter ini kita akan membandingkan tiga bentuk injection yang umum di Kotlin: constructor injection, setter injection, dan field injection. Pembaca akan melihat implementasi container manual sederhana, lalu container mirip framework yang umum dipakai di proyek Kotlin/Android.
Tiga Bentuk Dependency Injection #
Setiap bentuk injection memiliki trade-off. Berikut pemetaannya:
| Bentuk | Saatnya dipakai | Catatan |
|---|---|---|
| Constructor injection | Default; dependensi wajib | Immutable, mudah diuji, dapat divalidasi di constructor |
| Setter injection | Dependensi opsional atau dapat diganti | Object berada dalam keadaan setengah siap sampai setter dipanggil |
| Field injection | Hanya bila framework mengharuskan | Membuka state ke publik; sulit diuji tanpa refleksi |
Constructor injection sebaiknya menjadi pilihan default karena dependensi terlihat eksplisit di tanda tangan constructor, dan object dapat divalidasi seketika. Setter dan field injection hanya dipakai bila memang ada alasan kuat seperti dependensi opsional atau batasan framework.
Implementasi Constructor, Setter, dan Field Injection #
Contoh berikut memperlihatkan ketiga bentuk injection sekaligus pada tiga class Service1, Service2, dan Service3. Class-class tersebut bergantung pada abstraction ApiClient dengan tiga cara yang berbeda.
// Demo Dependency Injection: constructor, setter, field injection; manual vs framework.
interface ApiClient {
fun get(path: String): String
}
class HttpApiClient : ApiClient {
override fun get(path: String): String = "GET $path -> 200 OK"
}
class MockApiClient : ApiClient {
override fun get(path: String): String = "MOCK $path -> data"
}
class Service1 {
// 1. Constructor injection (rekomendasi)
private val api: ApiClient
constructor(api: ApiClient) {
this.api = api
}
fun jalankan() = println("Service1: ${api.get("/users")}")
}
class Service2 {
private lateinit var api: ApiClient
// 2. Setter injection
fun setApi(api: ApiClient) { this.api = api }
fun jalankan() = println("Service2: ${api.get("/orders")}")
}
class Service3 {
// 3. Field injection (anti-pola; hanya untuk demo)
var api: ApiClient? = null
fun jalankan() {
val a = api ?: throw IllegalStateException("api belum diset")
println("Service3: ${a.get("/items")}")
}
}
// === Manual container sederhana ===
class ContainerManual {
val instances = mutableMapOf<String, Any>()
inline fun <reified T : Any> register(instance: T) {
instances[T::class.simpleName ?: "Unknown"] = instance
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Any> resolve(): T =
instances[T::class.simpleName] as T
}
// === Framework-like container (mirip Koin module) ===
object AppContainer {
lateinit var apiClient: ApiClient
lateinit var service1: Service1
lateinit var service2: Service2
fun init(api: ApiClient) {
apiClient = api
service1 = Service1(apiClient)
service2 = Service2().also { it.setApi(apiClient) }
}
}
fun main() {
println("=== Demo Dependency Injection ===")
println("--- 1. Constructor injection ---")
val s1 = Service1(HttpApiClient())
s1.jalankan()
println()
println("--- 2. Setter injection ---")
val s2 = Service2()
s2.setApi(MockApiClient())
s2.jalankan()
println()
println("--- 3. Field injection (kurang disarankan) ---")
val s3 = Service3()
s3.api = HttpApiClient()
s3.jalankan()
println()
println("--- 4. Manual container ---")
val container = ContainerManual()
val api = HttpApiClient()
container.register(api)
container.register(Service1(api))
container.resolve<Service1>().jalankan()
println()
println("--- 5. Framework-like container ---")
AppContainer.init(MockApiClient())
AppContainer.service1.jalankan()
AppContainer.service2.jalankan()
}Output yang Dihasilkan:
=== Demo Dependency Injection ===
--- 1. Constructor injection ---
Service1: GET /users -> 200 OK
--- 2. Setter injection ---
Service2: MOCK /orders -> data
--- 3. Field injection (kurang disarankan) ---
Service3: GET /items -> 200 OK
--- 4. Manual container ---
Service1: GET /users -> 200 OK
--- 5. Framework-like container ---
Service1: MOCK /users -> data
Service2: MOCK /orders -> dataBedah Kode Baris-per-Baris #
Mari kita perhatikan bagian-bagian kunci:
- Baris 15-22 (
Service1): Constructor menerimaApiClientdan menyimpannya ke fieldval. Setelah object dibuat, dependensi tidak bisa diubah lagi; ini bentuk injection yang aman. - Baris 24-29 (
Service2): Field dideklarasikan sebagailateinit var. MethodsetApimengisi dependensi setelah object dibuat. Resikonya: jikajalankandipanggil sebelumsetApi, runtime melemparkanUninitializedPropertyAccessException. - Baris 31-38 (
Service3): Fieldapibersifat nullable dan dapat ditulis dari luar. Methodjalankanmemeriksa null secara eksplisit dengan elvis operator sebelum memakai nilai. - Baris 41-51 (
ContainerManual): Container sederhana memetakan nama class ke instance. Methodregisterdanresolvememakaireifiedagar tipe bisa diakses pada runtime. - Baris 54-64 (
AppContainer): Object tunggal menyimpan semua layanan utama aplikasi. Methodinitberperan sebagai “composition root” yang merakit seluruh grafik dependensi.
Manual vs Framework #
Container manual cocok untuk aplikasi kecil hingga menengah karena tidak menambah dependensi. Untuk aplikasi besar atau Android, framework DI biasanya lebih efisien:
| Pendekatan | Kelebihan | Kekurangan |
|---|---|---|
| Manual container | Tanpa dependensi, transparan | Resolusi manual; runtime error jika lupa register |
| Koin (Kotlin DSL) | Ringan, tidak ada code generation | Resolusi tetap runtime |
| Dagger / Hilt | Compile-time validation, performance tinggi | Belokan curam; butuh annotation processor |
| Spring DI | Matang, banyak fitur | Berat untuk non-Spring app |
Penutup #
Dependency Injection memberi mekanisme konkret untuk menerapkan Dependency Inversion. Constructor injection menjadi default, sementara container manual atau framework mengorkestrasi injeksi di composition root. Pada chapter berikutnya kita akan membahas Composition over Inheritance: pendekatan alternatif dalam menyusun behavior yang lebih fleksibel dibanding hierarki class mendalam.
Referensi #
Referensi Lanjutan #
- Kotlin documentation home
— halaman utama dokumentasi Kotlin untuk merancang constructor injection dengan
classdaninterface