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:

BentukSaatnya dipakaiCatatan
Constructor injectionDefault; dependensi wajibImmutable, mudah diuji, dapat divalidasi di constructor
Setter injectionDependensi opsional atau dapat digantiObject berada dalam keadaan setengah siap sampai setter dipanggil
Field injectionHanya bila framework mengharuskanMembuka 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.

kotlin
// 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:

text
=== 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 -> data

Bedah Kode Baris-per-Baris #

Mari kita perhatikan bagian-bagian kunci:

  1. Baris 15-22 (Service1): Constructor menerima ApiClient dan menyimpannya ke field val. Setelah object dibuat, dependensi tidak bisa diubah lagi; ini bentuk injection yang aman.
  2. Baris 24-29 (Service2): Field dideklarasikan sebagai lateinit var. Method setApi mengisi dependensi setelah object dibuat. Resikonya: jika jalankan dipanggil sebelum setApi, runtime melemparkan UninitializedPropertyAccessException.
  3. Baris 31-38 (Service3): Field api bersifat nullable dan dapat ditulis dari luar. Method jalankan memeriksa null secara eksplisit dengan elvis operator sebelum memakai nilai.
  4. Baris 41-51 (ContainerManual): Container sederhana memetakan nama class ke instance. Method register dan resolve memakai reified agar tipe bisa diakses pada runtime.
  5. Baris 54-64 (AppContainer): Object tunggal menyimpan semua layanan utama aplikasi. Method init berperan 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:

PendekatanKelebihanKekurangan
Manual containerTanpa dependensi, transparanResolusi manual; runtime error jika lupa register
Koin (Kotlin DSL)Ringan, tidak ada code generationResolusi tetap runtime
Dagger / HiltCompile-time validation, performance tinggiBelokan curam; butuh annotation processor
Spring DIMatang, banyak fiturBerat 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 class dan interface