Pilihan antara pub dan privat terasa cukup selama crate kita kecil. Namun pada proyek bercabang banyak seperti struktur warung pada Chapter sebelumnya, kebutuhan nyata muncul di tengah-tengah: fungsi diskon perlu dipakai oleh seluruh module di crate kita, tetapi tidak layak diperlihatkan kepada pemakai luar yang mengimpor crate. Menandainya pub memboroskan antarmuka; membiarkannya privat memaksa kita menduplikasi logika. Rust menyediakan tingkatan di antara keduanya lewat anotasi visibility bertingkat: pub(crate), pub(super), dan pub(in path). Chapter penutup Section ini membedah ketiganya pada crate library sungguhan, memverifikasi batas-batasnya lewat cargo test, dan melihat wujud error ketika batas dilampaui.

pub(crate): Publik di dalam Crate Ini Saja #

Mekanismenya lurus: pub(crate) membuka item untuk seluruh module dalam crate yang sama — termasuk module-module yang bukan turunannya — namun tetap privat bagi dunia luar. Fungsi bertanda ini bebas dipanggil dari module mana pun di crate, seolah pub; begitu crate dikompilasi sebagai library dan diimpor pihak lain, fungsi itu berperilaku privat penuh. Anotasi semacam ini lazim untuk helper internal, struct penolong antar-module, hingga seluruh module util yang tidak ingin menjadi API publik.

Kita bangun crate library kasir yang memakainya. Struktur proyeknya ringkas:

text
kasir/
├── Cargo.toml
└── src/
    ├── lib.rs
    └── pembayaran.rs

Berkas src/lib.rs menampung fungsi perhitungan inti, termasuk diskon yang hanya urusan internal crate:

rust
pub mod pembayaran;

pub fn hitung_total(harga_satuan: u32, jumlah: u32) -> u32 {
    harga_satuan * jumlah
}

pub(crate) fn terapkan_diskon(total: u32) -> u32 {
    if total >= 100000 {
        total - 10000
    } else {
        total
    }
}

pub fn tagihan_akhir(harga_satuan: u32, jumlah: u32) -> u32 {
    let total = hitung_total(harga_satuan, jumlah);
    terapkan_diskon(total)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn total_sesuai() {
        assert_eq!(hitung_total(25000, 2), 50000);
    }

    #[test]
    fn diskon_berlaku() {
        assert_eq!(terapkan_diskon(150000), 140000);
    }
}

Bedah kode di atas:

  1. Baris 3 (pub fn hitung_total(...)): fungsi publik sejati — bagian antarmuka crate kasir yang sah dipakai siapa pun.
  2. Baris 7 (pub(crate) fn terapkan_diskon(...)): fungsi diskon terbuka untuk seluruh isi crate ini. Module pembayaran, module test, dan fungsi apa pun di crate boleh memanggilnya; pemakai eksternal tidak.
  3. Baris 15 (pub fn tagihan_akhir(...)): pintu publik yang membungkus logika internal — pola khas crate sehat: fungsi pub menyusun hasil dari kombinasi helper pub(crate) dan privat.
  4. Baris 20 (#[cfg(test)]): unit test hanya dikompilasi saat menjalankan cargo test, bukan saat crate dipakai. Test pada Baris 30 membuktikan terapkan_diskon terlihat dari module test di crate yang sama.

pub(super) dan pub(in path) untuk Batas Lebih Ketat #

Dua anotasi lain membatasi lebih sempit dari pub(crate). pub(super) membuka item hanya untuk module induk langsung beserta turunannya; pub(in path) membuka item untuk module tertentu yang kita sebut path-nya beserta seluruh subtree-nya — misalnya pub(in crate::pembayaran) berlaku untuk semua kode di bawah cabang pembayaran. Keduanya hidup di module bersarang, jadi kita isi src/pembayaran.rs:

rust
pub mod qris {
    pub fn bayar(total: u32) {
        println!("QRIS: Rp{} berhasil dibayar", total);
    }
}

pub mod validasi {
    pub(super) fn nominal_valid(total: u32) -> bool {
        total > 0
    }
}

mod tarif {
    pub(in crate::pembayaran) fn layanan(total: u32) -> u32 {
        total / 100
    }
}

pub fn proses(total: u32) {
    if validasi::nominal_valid(total) {
        let biaya_layanan = tarif::layanan(total);
        qris::bayar(total + biaya_layanan);
    } else {
        println!("Nominal tidak valid, transaksi dibatalkan");
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn biaya_layanan_seperseratus() {
        assert_eq!(tarif::layanan(50000), 500);
    }

    #[test]
    fn nominal_nol_ditolak() {
        assert!(!validasi::nominal_valid(0));
        assert!(validasi::nominal_valid(1));
    }
}

Bedah kode di atas:

  1. Baris 8 (pub(super) fn nominal_valid(...)): fungsi di module validasi terlihat oleh module induknya — pembayaran — dan turunannya saja. Karena validasi sendiri bertanda pub mod, pemakai luar dapat melihat module-nya, tetapi fungsi nominal_valid tetap tersembunyi bagi mereka.
  2. Baris 14 (pub(in crate::pembayaran) fn layanan(...)): fungsi di module privat tarif dibuka untuk seluruh subtree pembayaran. Module tests berada di dalam subtree itu, sehingga pengujian pada Baris 34 sah.
  3. Baris 19 (pub fn proses(...)): fungsi orkestrator publik menggabungkan ketiga lapisan — memanggil validasi pub(super), tarif pub(in ...), dan qris publik. Pemakai luar cukup mengenal proses.
  4. Baris 20–22 (if validasi::nominal_valid(total)): pemanggilan dari module pembayaran sendiri — tepat wilayah yang diizinkan kedua anotasi.

Verifikasi Batas lewat cargo test #

Klaim-klaim di atas bukan sekadar janji — kita bisa membuktikannya dengan cargo test. Perintah ini mengompilasi crate beserta seluruh module testnya lalu menjalankan semuanya:

bash
cargo test

Output yang Dihasilkan:

text
running 4 tests
test pembayaran::tests::nominal_nol_ditolak ... ok
test pembayaran::tests::biaya_layanan_seperseratus ... ok
test tests::diskon_berlaku ... ok
test tests::total_sesuai ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Empat test lolos — dua dari module test di lib.rs yang memanggil terapkan_diskon bertanda pub(crate), dan dua dari module test di pembayaran.rs yang memanggil item bertanda pub(super) serta pub(in ...). Di depan baris running 4 tests, cargo juga menampilkan status kompilasi, dan di bagian akhir selalu mengikuti Doc-tests kasir yang menjalankan contoh kode pada dokumentasi — untuk crate kita masih kosong.

Ketika Batas Dilampaui dari Luar Crate #

Bukti kedua: akses dari luar crate memang tertolak. Integration test pada folder tests/ dikompilasi sebagai crate terpisah yang mengimpor kasir layaknya pemakai eksternal, sehingga cocok menjadi penguji batas. Berkas tests/akses_eksternal.rs berikut mencoba mengimpor fungsi internal:

rust
use kasir::terapkan_diskon;

#[test]
fn akses_diskon_dari_luar() {
    assert_eq!(terapkan_diskon(150000), 140000);
}

Pesan error ketika cargo test dijalankan (baris status Compiling di depan memuat lokasi proyek yang berbeda-beda di tiap komputer, jadi tidak dikutip):

text
error[E0603]: function `terapkan_diskon` is private
 --> tests/akses_eksternal.rs:1:12
  |
1 | use kasir::terapkan_diskon;
  |            ^^^^^^^^^^^^^^^ private function
  |
note: the function `terapkan_diskon` is defined here
 --> src/lib.rs:7:1
  |
7 | pub(crate) fn terapkan_diskon(total: u32) -> u32 {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `kasir` (test "akses_eksternal") due to 1 previous error
warning: build failed, waiting for other jobs to finish...

Pesan E0603 yang sama dengan kasus privat biasa muncul — dari luar crate, pub(crate) memang tampak sebagai fungsi privat. Perhatikan baris catatan: kompilator menunjuk persis deklarasi pub(crate) di lib.rs, jadi sumber penolakannya jelas. Baris terakhir warning: build failed, waiting for other jobs to finish... adalah pesan cargo bahwa target lain yang sedang dikompilasi paralel ditunggu sampai selesai sebelum cargo berhenti. Hapus berkas test itu untuk mengembalikan proyek ke hijau.

Batas pub(super) juga bisa kita patahkan dari dalam crate sendiri. Tambahkan fungsi berikut di baris paling akhir src/lib.rs, lalu jalankan cargo build:

rust
pub fn cek_validasi(total: u32) -> bool {
    crate::pembayaran::validasi::nominal_valid(total)
}

Pesan error kompilator:

text
error[E0603]: function `nominal_valid` is private
  --> src/lib.rs:36:34
   |
36 |     crate::pembayaran::validasi::nominal_valid(total)
   |                                  ^^^^^^^^^^^^^ private function
   |
note: the function `nominal_valid` is defined here
  --> src/pembayaran.rs:8:5
   |
 8 |     pub(super) fn nominal_valid(total: u32) -> bool {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `kasir` (lib) due to 1 previous error

Error yang sama, penolak yang berbeda: crate root bukan wilayah super dari module validasi — super dari validasi adalah pembayaran — sehingga fungsi itu tidak terlihat dari lokasi baru. Nomor baris 36 pada pesan mengikuti posisi fungsi tambahan di akhir lib.rs. Anotasi visibility menempel pada posisi deklarasi, bukan pada niat pemanggil.

Ringkasan Tiga Anotasi #

Tabel berikut merangkum wilayah kerja setiap anotasi pada deklarasi pub(...):

AnotasiTerlihat olehContoh wajar penggunaan
pub(crate)Seluruh module dalam crate iniHelper lintas module, module util internal
pub(super)Module induk langsung dan turunannyaStep internal milik module anak
pub(in crate::x)Subtree module x yang disebutFungsi khusus satu cabang besar

Ringkasan #

Anotasi visibility bertingkat mengisi ruang antara pub dan privat: pub(crate) membuka item untuk seluruh isi crate namun tertutup bagi pemakai luar, pub(super) membatasi pada module induk, dan pub(in path) membatasi pada subtree tertentu. Batas-batas itu terverifikasi lewat cargo test dari dalam dan error E0603 dari luar crate — bukti bahwa promise visibility ditegakkan compiler, bukan konvensi. Dengan ini perjalanan Section Modul dan Visibilitas tuntas: dari mod pertama hingga antarmuka berjenjang, kita kini mampu menyusun crate yang rapi di dalam dan terkendali dari luar — bekal yang langsung terpakai ketika Section berikutnya membahas crate sebagai unit distribusi bersama Cargo.