Setelah memahami package, modul, dan internal, kita siap merancang library yang sehat. Tujuan utamanya: mengekspos permukaan API sekecil mungkin sambil tetap fleksibel untuk perubahan internal. Pada chapter ini kita akan membahas prinsip desain library, pola pemisahan api/main/test, dan contoh konkret yang memadukan semuanya.
Prinsip Desain Library #
Library yang baik mengikuti beberapa prinsip:
- Minimalkan permukaan publik: hanya ekspos apa yang menjadi kontrak API. Setiap deklarasi
publicmenjadi komitmen — mengubahnya berarti breaking change. - Sembunyikan implementasi: gunakan
internaluntuk helper, repo, dan detail teknis. Implementasi bebas diubah tanpa memengaruhi konsumen. - Pakai fasad: satu object atau class
publicmenjadi pintu masuk. Detail di baliknya tidak tampak dari luar. - Versi semantik: setiap rilis library mengikuti
MAJOR.MINOR.PATCH. Penambahan API publik menaikkanMINOR; perubahan breaking menaikkanMAJOR. - Dokumentasi KDoc: setiap deklarasi publik wajib memiliki KDoc yang menjelaskan kontrak dan contoh pemakaian singkat.
Organisasi Modul: api/main/test #
Pola yang sering dipakai pada library berskala menengah-besar adalah memisahkan modul ke tiga bagian:
| Modul | Isi | Contoh |
|---|---|---|
:api | Interface, data class, dan tipe publik. | interface KalkulatorPajak, data class RincianPajak |
:main (atau :impl) | Implementasi konkret, factory, helper internal. | internal class PenggunaRepositoryImpl |
:test | Kode uji untuk kedua modul di atas. | class KalkulatorPajakTest |
Pola ini memungkinkan konsumen hanya bergantung pada :api dan tidak peduli dengan detail :main. Saat implementasi berganti (misalnya dari in-memory ke database), kontrak :api tetap.
Contoh Kode #
Kode berikut memperlihatkan library kecil yang memisahkan API publik dari implementasi internal:
// File: library_design.kt
// Mendemonstrasikan desain library: minimalkan permukaan publik,
// sembunyikan implementasi di balik internal.
package com.example.library
// === API PUBLIK ===
/** Titik masuk library. */
object KalkulatorPajak {
fun hitung(nilai: Long, wilayah: String): Long {
val tarif = ambilTarif(wilayah)
return hitungPajakInternal(nilai, tarif)
}
}
data class RincianPajak(val nilai: Long, val tarif: Double, val pajak: Long)
/** Fasad publik untuk rincian lengkap. */
object PajakRincian {
fun rinci(nilai: Long, wilayah: String): RincianPajak {
val tarif = ambilTarif(wilayah)
val pajak = hitungPajakInternal(nilai, tarif)
return RincianPajak(nilai, tarif, pajak)
}
}
// === IMPLEMENTASI INTERNAL ===
internal fun ambilTarif(wilayah: String): Double {
return when (wilayah.lowercase()) {
"jakarta" -> 0.11
"bandung" -> 0.10
"surabaya" -> 0.09
else -> 0.08
}
}
internal fun hitungPajakInternal(nilai: Long, tarif: Double): Long {
return (nilai * tarif).toLong()
}
// === DEMO ===
fun main() {
val nilai = 1_000_000L
val wilayah = "Jakarta"
println("KalkulatorPajak.hitung($nilai, $wilayah) = ${KalkulatorPajak.hitung(nilai, wilayah)}")
val rincian = PajakRincian.rinci(nilai, "Bandung")
println("Rincian: $rincian")
println("Struktur permukaan API:")
println(" publik: KalkulatorPajak, PajakRincian, RincianPajak")
println(" internal: ambilTarif(), hitungPajakInternal()")
}Output yang Dihasilkan:
KalkulatorPajak.hitung(1000000, Jakarta) = 110000
Rincian: RincianPajak(nilai=1000000, tarif=0.1, pajak=100000)
Struktur permukaan API:
publik: KalkulatorPajak, PajakRincian, RincianPajak
internal: ambilTarif(), hitungPajakInternal()Mari kita bedah kode di atas:
- Baris 11-15 (
object KalkulatorPajak): fasad publik. Konsumen library hanya memanggilKalkulatorPajak.hitung(...). Methodhitungsendiri memanggil dua functioninternal. - Baris 17 (
data class RincianPajak): tipe publik untuk hasil terperinci. Karenadata class, otomatis menghasilkanequals,hashCode, dantoString. Konsumen dapat membaca field-nya tetapi tidak melihat cara pajak dihitung. - Baris 20-26 (
object PajakRincian): fasad kedua untuk mendapatkan rincian lengkap. MemisahkanKalkulatorPajak(cepat, hanya nominal) danPajakRincian(lengkap) menjaga API tetap modular. - Baris 30-38 (
internal fun ambilTarif): lookup tarif per wilayah. Internal karena tarif bisa berubah tanpa memengaruhi kontrak API. Jika ke depan perlu mengekspos tarif ke konsumen, kita dapat memindahkan kepublictanpa breaking change lain. - Baris 40-42 (
internal fun hitungPajakInternal): helper matematika. Internal karena formula pajak adalah detail teknis. - Baris 45-56 (
fun main): demonstrasi. Memanggil kedua fasad dan mencetak struktur API untuk memperjelas batas public/internal.
Strategi Mempertahankan Kompatibilitas #
Saat library dipakai banyak konsumen, kita perlu strategi untuk mengubah API tanpa merusak pemakaian lama:
- Tambah dengan default: menambah parameter function dengan default value tidak merusak pemakaian lama.
- Deprecate bertahap: tandai function lama dengan
@Deprecated("gunakan x()", ReplaceWith("x()"))dan biarkan satu atau dua rilis sebelum dihapus. - Adopsi sealed interface: untuk representasi state, sealed interface memungkinkan penambahan cabang baru sebagai
data objecttanpa menggangguwhenekshaustif pada konsumen (selama konsumen memakaielse).
Referensi Lanjutan #
- Packages — konteks struktur package untuk desain library di panduan resmi
- Visibility modifiers — panduan modifier visibility sebagai API surface library
- What’s new in Kotlin 2.0 — catatan rilis terkait evolusi modul dan library Kotlin
Selanjutnya #
Section Packages dan Modul telah membahas struktur kode Kotlin dari level file hingga level modul. Section berikutnya, Annotations dan Reflection, akan membahas bagaimana menambah metadata ke kode dan membacanya kembali saat runtime.