Setelah memahami semua konsep generics — type parameter, bound, variance, type erasure, dan reified — kita perlu memadukannya menjadi desain API yang bersih dan mudah dipakai. Generics yang berlebihan dapat membuat signature function sulit dibaca, sementara generics yang terlalu sedikit menyebabkan duplikasi kode. Pada chapter ini kita akan membahas pedoman desain API generic, mengenal prinsip PECS (Producer Extends, Consumer Super), mempelajari kapan memakai generics vs interface, serta melihat bagaimana type aliases menyederhanakan signature generic kompleks.
Kapan Memakai Generics vs Interface #
Pilihan antara generics dan interface bergantung pada karakteristik masalah. Generics cocok ketika satu definisi harus bekerja untuk banyak tipe dengan struktur yang sama. Interface cocok ketika kita ingin mendefinisikan kontrak perilaku yang berbeda untuk setiap implementasi.
| Kriteria | Pakai Generics | Pakai Interface |
|---|---|---|
| Tipe data | Satu definisi untuk banyak tipe | Satu kontrak, banyak implementasi |
| Perilaku | Sama untuk semua tipe | Berbeda per implementasi |
| Contoh | Box<T>, List<E>, Repository<T> | Comparable<T>, Iterable<T> |
Pada banyak kasus, keduanya dipakai bersama — interface generic seperti Repository<T> mendefinisikan kontrak yang berlaku untuk banyak tipe.
Menghindari Over-Generic #
Salah satu kesalahan umum adalah menulis class dengan terlalu banyak type parameter. Semakin banyak type parameter, semakin sulit dipakai. Aturan praktis: jika sebuah class memiliki lebih dari tiga type parameter, pertimbangkan untuk memecahnya menjadi beberapa class yang lebih kecil.
// Hindari:
class Manager<A, B, C, D>(val a: A, val b: B, val c: C, val d: D)
// Pertimbangkan:
class Manager<A, B>(val a: A, val b: B)
class Pengolah<C, D>(val c: C, val d: D)Pemecahan class menjadi unit yang lebih kecil sering kali juga meningkatkan kohesi — setiap class menangani satu tanggung jawab.
Prinsip PECS #
PECS adalah akronim Java: Producer Extends, Consumer Super. Prinsip ini menjelaskan kapan memakai variance covariant dan contravariant:
- Producer Extends: jika class memproduksi
T, gunakanout T(covariant). Contoh:Iterator<out T>,List<out E>. - Consumer Super: jika class mengkonsumsi
T, gunakanin T(contravariant). Contoh:Comparator<in T>,Consumer<in T>.
Di Kotlin, prinsip ini diterjemahkan menjadi out untuk producer dan in untuk consumer. Menerapkan variance dengan benar membuat API lebih fleksibel tanpa kehilangan type safety.
Type Aliases #
Type aliases memungkinkan kita memberi nama alternatif untuk tipe yang kompleks. Hal ini tidak membuat tipe baru — aliases hanyalah nama lain — tetapi membuat signature function lebih ringkas dan ekspresif.
typealias StringMap<V> = Map<String, V>
typealias PenggunaId = Long
typealias Proses<T> = (T) -> TType aliases sangat berguna untuk tipe generic bersarang seperti Map<String, List<Pengguna>> yang dapat disederhanakan menjadi PenggunaDaftar.
Contoh Lengkap dengan Berbagai Pola #
Mari kita lihat bagaimana berbagai pedoman desain diterapkan dalam kode nyata:
// Chapter: Designing Generic APIs
// Pedoman desain API generic, PECS principle, type aliases.
// Type alias untuk menyederhanakan signature generic kompleks
typealias StringMap<V> = Map<String, V>
typealias PenggunaId = Long
typealias Proses<T> = (T) -> T
// Desain 1: Generic untuk tipe wadah yang benar-benar generik
class Tumpukan<T> {
private val item = mutableListOf<T>()
fun dorong(x: T) = item.add(x)
fun ambil(): T? = if (item.isEmpty()) null else item.removeAt(item.lastIndex)
fun ukuran(): Int = item.size
}
// Desain 2: Interface ketika perilaku spesifik diperlukan
interface Sortable<T : Comparable<T>> {
fun sorted(): List<T>
}
class DaftarSkor(private val skor: List<Int>) : Sortable<Int> {
override fun sorted(): List<Int> = skor.sorted()
}
// PECS: Producer Extends, Consumer Super -> Kotlin: out untuk producer, in untuk consumer
class Pipeline<T>(
private var sumber: (() -> T)? = null,
private var konsumen: ((T) -> Unit)? = null
) {
fun setSumber(s: () -> T) { sumber = s }
fun setKonsumen(k: (T) -> Unit) { konsumen = k }
fun jalankan() {
val s = sumber ?: return
val k = konsumen ?: return
val data = s()
k(data)
}
}
// Anti-pattern: terlalu generik
// class Manager<A, B, C, D>(...) <- terlalu banyak type param
// Solusi: pisahkan tanggung jawab
class PenggunaRepository<T> {
private val db = mutableMapOf<Long, T>()
fun simpan(id: Long, item: T) { db[id] = item }
fun ambil(id: Long): T? = db[id]
fun semua(): List<T> = db.values.toList()
}
data class Produk(val id: Long, val nama: String, val harga: Int)
fun main() {
// Type alias dipakai sebagai pengganti tipe panjang
val konfigurasi: StringMap<Int> = mapOf("max" to 100, "min" to 5)
println("Konfigurasi: $konfigurasi")
val uid: PenggunaId = 12345L
println("Pengguna ID: $uid")
val ubah: Proses<String> = { it.uppercase() }
println("Proses 'halo': ${ubah("halo")}")
val tumpukan = Tumpukan<Int>()
tumpukan.dorong(1)
tumpukan.dorong(2)
tumpukan.dorong(3)
println("Tumpukan ukuran: ${tumpukan.ukuran()}")
println("Ambil: ${tumpukan.ambil()}")
val daftarSkor = DaftarSkor(listOf(70, 95, 80, 65, 90))
println("Skor terurut: ${daftarSkor.sorted()}")
val pipeline = Pipeline<String>()
pipeline.setSumber { "Budi" }
pipeline.setKonsumen { nama -> println("Halo, $nama!") }
pipeline.jalankan()
val repoProduk = PenggunaRepository<Produk>()
repoProduk.simpan(1L, Produk(1L, "Buku", 50000))
repoProduk.simpan(2L, Produk(2L, "Pulpen", 7500))
val semua = repoProduk.semua()
println("Total produk: ${semua.size}")
println("Produk id=1: ${repoProduk.ambil(1L)}")
}Output yang Dihasilkan:
Konfigurasi: {max=100, min=5}
Pengguna ID: 12345
Proses 'halo': HALO
Tumpukan ukuran: 3
Ambil: 3
Skor terurut: [65, 70, 80, 90, 95]
Halo, Budi!
Total produk: 2
Produk id=1: Produk(id=1, nama=Buku, harga=50000)Mari kita telaah baris-baris kode di atas:
typealias StringMap<V> = Map<String, V>: alias untukMap<String, V>yang sering muncul dalam kode. Setelah alias, kita dapat menulisStringMap<Int>sebagai penggantiMap<String, Int>.typealias PenggunaId = Long: alias sederhana untukLongyang merepresentasikan ID pengguna. Hal ini meningkatkan keterbacaan tanpa menambah abstraksi runtime.typealias Proses<T> = (T) -> T: alias untuk function type yang menerima dan mengembalikanT. Alias ini membuat signature function yang menerimaProses<String>lebih jelas.class Tumpukan<T>: class generic sederhana dengan satu type parameter. Tidak ada bound — generic bekerja untuk semua tipe.interface Sortable<T : Comparable<T>>: interface generic dengan recursive bound. Class yang mengimplementasikan harus menyediakan tipe yang dapat dibandingkan.class DaftarSkor : Sortable<Int>: implementasi denganT = Int. Class ini hanya bisa dibuat untuk skor integer — pertimbangan desain yang sederhana dan jelas.class Pipeline<T>dengan sumber dan konsumen: pipeline generik yang menghubungkan producer dan consumer. Memakai lambda nullable untuk fleksibilitas.class PenggunaRepository<T>: repository generik denganLongsebagai kunci. Berbeda denganRepository<T>pada chapter Generic Interfaces, class ini memakaiLonglangsung karena ID hampir selaluLongdi aplikasi nyata.
Pedoman Desain Tambahan #
Berikut beberapa pedoman tambahan saat mendesain API generic:
- Hindari bound yang tidak perlu — hanya tambahkan bound ketika kita benar-benar membutuhkan method dari tipe bound.
- Pilih nama type parameter deskriptif untuk signature publik —
Entitysering lebih jelas daripadaTpada library kompleks.
Referensi Lanjutan #
- Generics — rujukan resmi generics untuk mendesain API yang type-safe
Penutup #
Generics adalah alat yang kuat untuk menulis kode yang reusable dan type-safe. Dengan memahami type parameter, bound, variance, type erasure, dan reified, kita dapat membangun API yang fleksibel tanpa mengurangi keamanan tipe. Prinsip PECS memberi pedoman kapan memakai out dan in. Type aliases menyederhanakan signature kompleks. Pedoman desain yang baik menjaga keseimbangan antara generalitas dan keterbacaan.
Section Generics berakhir di sini. Pengetahuan tentang generics akan dipakai pada section berikutnya yang membahas lambdas dan functional programming, di mana banyak higher-order function memakai type parameter generic untuk menjaga fleksibilitas.