Dependency Inversion adalah prinsip desain yang menegaskan arah dependensi antar modul. Prinsip ini menyatakan bahwa modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah; keduanya harus bergantung pada abstraction. Pada chapter ini kita akan membedah anti-pola ketergantungan langsung pada concretion, lalu membangun ulang strukturnya dengan bantuan interface di Kotlin. Pembaca akan melihat bagaimana mengganti implementasi database dari MySQL ke Postgres atau InMemory cukup dilakukan di titik injeksi tanpa menyentuh kode layanan.

Masalah Ketergantungan pada Concretion #

Bayangkan sebuah layanan yang langsung membuat instance MysqlDatabase di dalam constructor-nya. Layanan tersebut tidak dapat diuji tanpa database nyata dan tidak dapat diganti tanpa mengubah kodenya. Kondisi ini melanggar Dependency Inversion karena modul tingkat tinggi (layanan) bergantung pada modul tingkat rendah (database konkret).

kotlin Contoh: anti-pola ketergantungan langsung
class MysqlDatabase {
    fun simpan(data: String) = println("[MySQL] simpan $data")
}
class LayananBuruk {
    private val db = MysqlDatabase()  // langsung membuat concretion
    fun simpanUser(nama: String) = db.simpan("user=$nama")
}

Layanan LayananBuruk di atas terikat erat dengan MysqlDatabase. Mengganti database berarti mengubah kode internal layanan, yang melanggar prinsip open-closed sekaligus inversion.

Solusi: Bergantung pada Abstraction #

Kita perkenalkan interface Database yang menjadi titik kontrak antara layanan dan implementasi penyimpanan. Layanan tingkat tinggi hanya tahu interface, sementara implementasi konkret (MySQL, Postgres, InMemory) menjadi object yang diinjeksikan dari luar.

kotlin
// Demo Dependency Inversion: high-level module bergantung pada abstraction.

// === ANTI-POLA: high-level tergantung pada concretion ===
class MysqlDatabase {
    fun simpan(data: String) = println("[MySQL] simpan $data")
}
class LayananBuruk {
    private val db = MysqlDatabase()  // ketergantungan langsung pada concretion
    fun simpanUser(nama: String) = db.simpan("user=$nama")
}

// === DIP: high-level tergantung pada abstraction ===
interface Database {
    fun simpan(data: String)
    fun ambil(key: String): String?
}

class MysqlDb : Database {
    override fun simpan(data: String) = println("[MySQL] simpan $data")
    override fun ambil(key: String): String? {
        println("[MySQL] ambil $key")
        return "data for $key"
    }
}

class PostgresDb : Database {
    override fun simpan(data: String) = println("[Postgres] simpan $data")
    override fun ambil(key: String): String? {
        println("[Postgres] ambil $key")
        return "data for $key"
    }
}

class InMemoryDb : Database {
    private val map = mutableMapOf<String, String>()
    override fun simpan(data: String) {
        val key = "key-${map.size + 1}"
        map[key] = data
        println("[InMemory] simpan $data ke $key")
    }
    override fun ambil(key: String): String? = map[key]
}

// Layanan hanya tahu Database (abstraction), tidak peduli MySQL/Postgres/InMemory
class LayananBagus(private val db: Database) {
    fun simpanUser(nama: String) = db.simpan("user=$nama")
    fun ambilUser(key: String): String? = db.ambil(key)
}

fun main() {
    println("=== Demo Dependency Inversion ===")

    println("--- Anti-pola (tergantung concretion) ---")
    LayananBuruk().simpanUser("Budi")

    println()
    println("--- DIP: pakai MySQL ---")
    val svcMysql = LayananBagus(MysqlDb())
    svcMysql.simpanUser("Siti")

    println()
    println("--- DIP: ganti ke Postgres tanpa ubah Layanan ---")
    val svcPg = LayananBagus(PostgresDb())
    svcPg.simpanUser("Dewi")

    println()
    println("--- DIP: pakai InMemory (untuk test) ---")
    val svcMem = LayananBagus(InMemoryDb())
    svcMem.simpanUser("Andi")
    svcMem.simpanUser("Rina")
    println("Ambil: ${svcMem.ambilUser("key-1")}")
    println("Ambil: ${svcMem.ambilUser("key-2")}")
}

Output yang Dihasilkan:

text
=== Demo Dependency Inversion ===
--- Anti-pola (tergantung concretion) ---
[MySQL] simpan user=Budi

--- DIP: pakai MySQL ---
[MySQL] simpan user=Siti

--- DIP: ganti ke Postgres tanpa ubah Layanan ---
[Postgres] simpan user=Dewi

--- DIP: pakai InMemory (untuk test) ---
[InMemory] simpan user=Andi ke key-1
[InMemory] simpan user=Rina ke key-2
Ambil: user=Andi
Ambil: user=Rina

Bedah Kode Baris-per-Baris #

Mari kita amati bagian-bagian penting dari kode di atas:

  1. Baris 13-16 (interface Database): Kita mendefinisikan abstraction berupa interface. Inilah “titik balik” dependensi: semua pihak sepakat berinteraksi melalui interface ini.
  2. Baris 18-42 (MysqlDb, PostgresDb, InMemoryDb): Tiga implementasi konkret dengan perilaku berbeda. Setiap class meng-override simpan dan ambil.
  3. Baris 45-48 (LayananBagus): Constructor menerima Database, bukan class konkret. Layanan tidak tahu apakah data disimpan ke MySQL atau memori; ia hanya memanggil method interface.
  4. Baris 60-63 (val svcPg = LayananBagus(PostgresDb())): Pemanggil membuat instance PostgresDb dan menyuntikkannya ke layanan. Inilah titik injeksi: kode aplikasi memilih implementasi konkret tanpa mengubah kode layanan.

Manfaat Praktis #

Inversion membawa tiga manfaat konkret. Pertama, pengujian menjadi mudah: kita menyuntikkan InMemoryDb saat unit test, tanpa perlu server MySQL. Kedua, mengganti vendor storage hanya menyentuh satu titik (composition root). Ketiga, layanan tingkat tinggi dapat dikembangkan tanpa menunggu implementasi infrastruktur siap; cukup buat stub yang mengikuti interface.

Kaitan dengan Dependency Injection #

Dependency Inversion adalah prinsip desain (apa), sementara Dependency Injection adalah teknik (bagaimana). Pada chapter berikutnya kita akan membahas Dependency Injection secara penuh: constructor injection, setter injection, field injection, serta perbandingan antara container manual dengan framework seperti Koin atau Dagger.

Referensi #

Referensi Lanjutan #

  • Kotlin documentation home — halaman utama dokumentasi Kotlin untuk memahami interface sebagai abstraksi yang membalik arah dependensi