Saat membangun library, kita ingin menyembunyikan implementasi tanpa harus membuatnya benar-benar privat. Modifier internal menjawab kebutuhan itu: deklarasi tetap dapat dipanggil dari seluruh kode dalam modul yang sama, tetapi tidak diekspos ke konsumen library. Pada chapter ini kita akan membahas cara kerja internal, kapan memakainya, dan fakta bahwa kode uji dalam modul yang sama tetap dapat memanggilnya — sebuah keuntungan besar dibanding Java.

Cara Kerja internal #

Modifier internal membatasi visibilitas deklarasi ke satu modul Kotlin. Berikut karakteristik pentingnya:

  • internal pada function, property, atau class top-level berarti hanya kode dalam modul yang sama yang dapat memanggilnya.
  • internal pada member class juga membatasi panggilan ke modul yang sama.
  • Pada level bytecode JVM, internal di-render sebagai public dengan nama yang di-mangle; panggilan dari kode Java menjadi tidak nyaman tetapi tetap mungkin.
  • Pada Kotlin/Native dan Kotlin/JS, batasan internal ditegakkan lebih ketat oleh kompilator.

Pola Umum: Public API + Internal Implementation #

Pola yang sering muncul pada desain library adalah: sebuah class atau function public bertindak sebagai fasad, sementara implementasi konkret diberi modifier internal. Konsumen hanya melihat API publik; detail implementasi tetap dapat diuji oleh kode uji dalam modul yang sama.

Contoh Kode #

Kode berikut memperlihatkan pola tersebut:

kotlin
// File: internal_visibility.kt
// Mendemonstrasikan cara kerja internal visibility dalam satu modul.
package com.example.internal

// Service public dengan repository internal.
interface PenggunaRepository {
    fun cari(id: Long): String
}

// Implementasi internal: tidak diekspos ke konsumen library.
internal class PenggunaRepositoryImpl : PenggunaRepository {
    override fun cari(id: Long): String {
        return "Pengguna#$id (dari database in-memory)"
    }
}

// Factory public: pengguna library hanya melihat factory, bukan impl.
object PenggunaFactory {
    fun buatRepository(): PenggunaRepository = PenggunaRepositoryImpl()
}

// Fungsi internal helper untuk diuji dalam modul yang sama.
internal fun normalisasiEmail(input: String): String {
    return input.trim().lowercase()
}

class LayananPengguna {
    private val repo: PenggunaRepository = PenggunaFactory.buatRepository()

    fun ambilNama(id: Long): String {
        return repo.cari(id)
    }

    fun catatEmail(email: String): String {
        // normalisasiEmail dapat dipanggil karena satu modul.
        val bersih = normalisasiEmail(email)
        return "Email $bersih dicatat."
    }
}

fun main() {
    val layanan = LayananPengguna()
    println(layanan.ambilNama(101L))
    println(layanan.catatEmail("  [email protected]  "))

    // Demonstrasi boundary internal: kita bisa panggil dari sini
    // karena main() juga berada dalam modul yang sama dengan deklarasi.
    println("Hasil normalisasi: ${normalisasiEmail("[email protected]")}")
}

Output yang Dihasilkan:

text
Pengguna#101 (dari database in-memory)
Email [email protected] dicatat.
Hasil normalisasi: [email protected]

Mari kita amati baris-baris di atas:

  1. Baris 6 (interface PenggunaRepository): kontrak publik. Konsumen library melihat interface ini.
  2. Baris 11 (internal class PenggunaRepositoryImpl): implementasi konkret yang internal. Konsumen library tidak dapat meng-instantiate class ini langsung. Implementasi dapat diganti tanpa memengaruhi API publik.
  3. Baris 19 (object PenggunaFactory): factory public. Satu-satunya jalur resmi untuk membuat instance PenggunaRepository. Memanggil PenggunaRepositoryImpl() di dalamnya sah karena berada dalam modul yang sama.
  4. Baris 28 (internal fun normalisasiEmail): helper internal. Hanya dipanggil dari LayananPengguna.catatEmail (baris 38) dan main() (baris 49), keduanya dalam modul yang sama.
  5. Baris 33 (class LayananPengguna): service public. Pengguna library memanggil ambilNama dan catatEmail, tetapi tidak melihat detail repo.
  6. Baris 42-49 (fun main): titik masuk. Mengamati bahwa normalisasiEmail dapat dipanggil dari main() karena main berada dalam modul yang sama dengan deklarasi internal.

Akses dari Kode Uji #

Salah satu keuntungan utama internal adalah kode uji dapat memanggil deklarasi internal selama berada dalam modul yang sama. Pada Gradle, source set test berada dalam modul yang sama dengan main, sehingga file uji pada src/test/kotlin dapat memanggil internal fun normalisasiEmail tanpa batasan. Hal ini berbeda dengan Java yang memakai package-private (default visibility) — yang membatasi berdasarkan package, bukan modul.

Kapan Tidak Memakai internal #

internal bukan solusi tunggal. Berikut kondisi ketika modifier lain lebih tepat:

  • Detail yang hanya dipakai dalam satu class: pakai private.
  • Helper yang hanya dipakai dalam satu file: pakai top-level private.
  • Class abstract yang menjadi bagian API publik: tetap public/abstract.
  • Function yang menjadi bagian kontrak API: tetap public.

Referensi Lanjutan #

Selanjutnya #

Dengan internal, kita memiliki dasar untuk membangun library yang bersih. Chapter Library Design akan menyatukan semua konsep ini menjadi pola desain library yang sehat.