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.
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:
// 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:
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:
data class Penggunadandata class Produk: dua model domain dengan struktur berbeda. Keduakan disimpan oleh repository yang berbeda, namun kontraknya sama.interface Repository<T>: kontrak dengan tiga method. Type parameterTmengikat tipe yang disimpan dan dikembalikan.class UserRepo : Repository<Pengguna>: kita memilihT = Pengguna. Kompilator kemudian mengharuskanfindmengembalikanPengguna?,findAllmengembalikanList<Pengguna>, dansavemenerimaPengguna.override fun find(id: Int): Pengguna?: signature override harus persis sama dengan versi interface setelahTdigantiPengguna. Jika signature berbeda, kompilator menolak.class ProdukRepo : Repository<Produk>: implementasi lain dengan tipe berbeda. Inilah inti generic — satu kontrak, banyak implementasi dengan tipe berbeda, semuanya type-safe.class BacaPengguna(private val sumber: UserRepo) : Readable<Pengguna>: class ini mengimplementasikan interfaceReadable<T>sambil menerimaUserReposebagai dependensi. Pola ini disebut delegation ringan.userRepo.save(Pengguna(1, "Budi", ...)): kompilator memverifikasi argumen bertipePengguna. Jika kita mencoba menyimpanProdukkeUserRepo, 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.
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
Tpada 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.