Interface memungkinkan kita mendefinisikan kontrak tanpa menentukan implementasi. Ketika kontrak tersebut perlu bekerja untuk berbagai tipe data, kita memakai generic interface — interface yang memiliki type parameter. Pola ini sangat umum pada arsitektur berlapis, misalnya pola Repository yang menyimpan entitas bertipe T. Dengan generic interface, kita dapat menulis interface Repository<T> sekali dan mengimplementasikannya sebagai UserRepo, ProdukRepo, atau PesananRepo tanpa mengulang signature. Pada chapter ini kita akan mendeklarasikan generic interface, mengimplementasikannya dengan tipe konkret, serta melihat bagaimana type parameter membantu kompilator memverifikasi setiap pemanggilan method.

Deklarasi Generic Interface #

Bentuk deklarasi generic interface mirip dengan class generic — type parameter ditulis setelah nama interface. Method-method di dalamnya dapat memakai type parameter sebagai tipe parameter atau tipe kembalian.

kotlin Interface Repository<T>
interface Repository<T> {
    fun find(id: Int): T?
    fun findAll(): List<T>
    fun save(item: T): T
}

Pada contoh di atas, type parameter T muncul di tiga tempat: sebagai tipe kembalian find (nullable T?), sebagai elemen List<T> pada findAll, dan sebagai tipe parameter sekaligus tipe kembalian save. Semua ini akan diikat ke tipe konkret saat kita mengimplementasikan interface.

Implementasi dengan Tipe Konkret #

Saat sebuah class mengimplementasikan generic interface, kita wajib menentukan type argument konkret. Kompilator kemudian memverifikasi setiap method sesuai dengan tipe yang kita pilih. Mari kita lihat implementasinya:

kotlin
// Chapter: Generic Interfaces
// Interface generic dan implementasinya.

data class Pengguna(val id: Int, val nama: String, val email: String)

data class Produk(val id: Int, val nama: String, val harga: Int)

interface Repository<T> {
    fun find(id: Int): T?
    fun findAll(): List<T>
    fun save(item: T): T
}

interface Readable<T> {
    fun baca(id: Int): T
}

class UserRepo : Repository<Pengguna> {
    private val db = mutableMapOf<Int, Pengguna>()

    override fun find(id: Int): Pengguna? = db[id]

    override fun findAll(): List<Pengguna> = db.values.toList()

    override fun save(item: Pengguna): Pengguna {
        db[item.id] = item
        return item
    }
}

class ProdukRepo : Repository<Produk> {
    private val daftar = mutableListOf<Produk>()

    override fun find(id: Int): Produk? = daftar.firstOrNull { it.id == id }

    override fun findAll(): List<Produk> = daftar.toList()

    override fun save(item: Produk): Produk {
        daftar.add(item)
        return item
    }
}

class BacaPengguna(private val sumber: UserRepo) : Readable<Pengguna> {
    override fun baca(id: Int): Pengguna {
        return sumber.find(id) ?: error("Pengguna id=$id tidak ditemukan")
    }
}

fun main() {
    val userRepo = UserRepo()
    userRepo.save(Pengguna(1, "Budi", "[email protected]"))
    userRepo.save(Pengguna(2, "Siti", "[email protected]"))

    val ditemukan = userRepo.find(1)
    println("Ditemukan: $ditemukan")

    val semua = userRepo.findAll()
    println("Total pengguna: ${semua.size}")

    val pembaca = BacaPengguna(userRepo)
    println("Baca pengguna id=2: ${pembaca.baca(2)}")

    val produkRepo = ProdukRepo()
    produkRepo.save(Produk(101, "Buku Kotlin", 75000))
    produkRepo.save(Produk(102, "Mouse", 120000))

    val p = produkRepo.find(101)
    println("Produk: $p")
    println("Semua produk: ${produkRepo.findAll()}")
}

Output yang Dihasilkan:

text
Ditemukan: Pengguna(id=1, nama=Budi, [email protected])
Total pengguna: 2
Baca pengguna id=2: Pengguna(id=2, nama=Siti, [email protected])
Produk: Produk(id=101, nama=Buku Kotlin, harga=75000)
Semua produk: [Produk(id=101, nama=Buku Kotlin, harga=75000), Produk(id=102, nama=Mouse, harga=120000)]

Mari kita telaah baris-baris kode di atas:

  1. data class Pengguna dan data class Produk: dua model domain dengan struktur berbeda. Keduakan disimpan oleh repository yang berbeda, namun kontraknya sama.
  2. interface Repository<T>: kontrak dengan tiga method. Type parameter T mengikat tipe yang disimpan dan dikembalikan.
  3. class UserRepo : Repository<Pengguna>: kita memilih T = Pengguna. Kompilator kemudian mengharuskan find mengembalikan Pengguna?, findAll mengembalikan List<Pengguna>, dan save menerima Pengguna.
  4. override fun find(id: Int): Pengguna?: signature override harus persis sama dengan versi interface setelah T diganti Pengguna. Jika signature berbeda, kompilator menolak.
  5. class ProdukRepo : Repository<Produk>: implementasi lain dengan tipe berbeda. Inilah inti generic — satu kontrak, banyak implementasi dengan tipe berbeda, semuanya type-safe.
  6. class BacaPengguna(private val sumber: UserRepo) : Readable<Pengguna>: class ini mengimplementasikan interface Readable<T> sambil menerima UserRepo sebagai dependensi. Pola ini disebut delegation ringan.
  7. userRepo.save(Pengguna(1, "Budi", ...)): kompilator memverifikasi argumen bertipe Pengguna. Jika kita mencoba menyimpan Produk ke UserRepo, kompilator menolak sebelum program dijalankan.

Beberapa Interface Generic Sekaligus #

Sebuah class dapat mengimplementasikan beberapa interface generic secara bersamaan. Standard library Kotlin memakai pola ini pada List yang mengimplementasikan Collection<E> dan Iterable<E>. Type parameter pada setiap interface dapat berbeda — kompilator tetap memverifikasi setiap method sesuai interface masing-masing.

kotlin Implementasi beberapa interface generic
class DaftarLengkap<T> : Iterable<T>, Collection<T> {
    // implementasi semua method abstrak
    override fun iterator(): Iterator<T> = TODO()
    override val size: Int get() = TODO()
    // ...
}

Kapan Memakai Generic Interface #

Generic interface sangat berguna ketika kita menulis kontrak yang berlaku untuk banyak tipe domain — Repository, Factory, Mapper, atau Validator. Pola ini mengurangi duplikasi dan menjaga konsistensi signature antar implementasi. Sebaliknya, jika sebuah interface hanya berlaku untuk satu tipe, kita tidak perlu membuatnya generic.

Referensi Lanjutan #

  • Generics — panduan resmi generic interface dan type parameter T pada kontrak

Type parameter pada interface juga dapat dibatasi dengan upper bound, sama seperti class dan function generic. Pembatasan tersebut akan kita bedah tuntas pada chapter Upper Bounds.