Kita kini menguasai seluruh perkakas organisasi: mod, path, use, pub, re-export, hingga pemisahan berkas. Perkakas itu menunggu satu hal terakhir — keputusan arsitektur. Keputusan paling berpengaruh pada proyek Rust berukuran apa pun adalah memisahkan library dari binary: seluruh logika bisnis hidup di crate library yang bisa diuji dan dipakai ulang, sementara binary menipis menjadi pembaca argumen dan pemanggil library. Chapter ini menyusun proyek warung lengkap — module utama publik, module util internal, dan binary minimal — mengikuti konvensi yang dipakai komunitas Rust dan ribuan crate di crates.io.

Masalah: main.rs yang Menjadi Gudang #

Menaruh seluruh module di bawah src/main.rs memang sah, namun biayanya muncul saat proyek dewasa. Pertama, seluruh kode hanya bisa dijalankan sebagai program — tidak ada cara memakainya sebagai library, misalnya dari tool lain atau dari test terpisah yang mengimpor crate kita. Kedua, main.rs terpaksa merangkap dua peran sekaligus: titik masuk program dan crate root seluruh logika. Peran campuran itu membuat argumen CLI, logika bisnis, dan utilitas jadi berdesakan dalam satu berkas. Ketiga, pengujian menyulit: unit test terbaik menempel dekat fungsinya, sedangkan integrasi test pada folder tests/ hanya bisa mengimpor crate yang punya target library.

Solusi idiomatiknya satu kalimat: letakkan logika di src/lib.rs, biarkan src/main.rs setipis mungkin. Kedua berkas itu tetap satu package — satu Cargo.toml, satu folder target — namun membentuk dua crate: crate library bernama warung dan crate binary yang kebetulan juga bernama warung.

Package dengan Dua Crate #

Mekanisme di balik pemisahan itu perlu dipahami presisi. Sebuah package adalah unit manajemen Cargo: folder berisi Cargo.toml yang menyimpan nama, versi, dan dependensi. Package boleh memuat beberapa target — dan dua target istimewa dibaca langsung dari lokasi berkasnya: src/lib.rs otomatis menjadi target library, src/main.rs otomatis menjadi target binary. Nama library mengikuti nama package, sehingga package warung menghasilkan crate library bernama warung yang diimpor lewat use warung::....

Karena binary dan library adalah dua crate berbeda, binary memperlakukan library kita seperti crate luar: seluruh aturan visibility publik berlaku penuh — item tanpa pub di library tidak akan terlihat dari main.rs. Justru pemisahan inilah yang menuntut disiplin antarmuka: module util internal cukup dideklarasikan mod utils; tanpa pub, dan otomatis tersembunyi dari binary.

Kedua target itu dibangun dari satu perintah yang sama. cargo run mengompilasi kedua crate lalu menjalankan binary; cargo build mengompilasi keduanya tanpa menjalankan; dan cargo test mengompilasi library beserta seluruh testnya. Pemisahan ini memungkinkan pemakai lain memakai logika warung tanpa menjalankan program — cukup menambahkan warung sebagai dependensi di Cargo.toml mereka, dan seluruh antarmuka publik langsung tersedia lewat use warung::.... Sebuah package juga bisa menampung binary tambahan di folder src/bin/ bila satu program kurang, tetapi pola dasarnya tetap: satu library, satu binary utama.

Proyek warung dari Nol #

Kita bangun proyeknya. Struktur akhir mengikuti pola yang dipakai mayoritas crate publik:

text
warung/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── lib.rs
    ├── menu.rs
    ├── kitchen.rs
    └── utils.rs

Crate root library src/lib.rs mendeklarasikan seluruh module dan satu fungsi sambutan:

rust
pub mod kitchen;
pub mod menu;

mod utils;

pub fn sambut(nama: &str) {
    println!("Selamat datang di Warung Kita, {}!", nama);
}

Module util internal src/utils.rs tidak diekspor — hanya warga crate library yang boleh memakainya:

rust
pub fn format_rupiah(nilai: u32) -> String {
    format!("Rp{}", nilai)
}

Module utama src/menu.rs menyimpan tipe dan fungsi domain:

rust
use crate::utils::format_rupiah;

pub struct MenuItem {
    pub nama: String,
    pub harga: u32,
}

pub fn menu_favorit() -> Vec<MenuItem> {
    vec![
        MenuItem { nama: String::from("Nasi Goreng Spesial"), harga: 25000 },
        MenuItem { nama: String::from("Ayam Bakar Madu"), harga: 30000 },
    ]
}

pub fn tampilkan_menu() {
    for item in menu_favorit() {
        println!("{} - {}", item.nama, format_rupiah(item.harga));
    }
}

Module utama src/kitchen.rs menangani proses memasak:

rust
pub fn masak(nama_pesanan: &str) {
    println!("Dapur memasak: {}", nama_pesanan);
}

Terakhir, binary src/main.rs menipis menjadi orkestrator:

rust
use warung::kitchen;
use warung::menu;

fn main() {
    warung::sambut("Budi");
    menu::tampilkan_menu();
    kitchen::masak("Nasi Goreng Spesial");
}

Jalankan seperti biasa:

bash
cargo run

Output yang Dihasilkan:

text
Selamat datang di Warung Kita, Budi!
Nasi Goreng Spesial - Rp25000
Ayam Bakar Madu - Rp30000
Dapur memasak: Nasi Goreng Spesial

Mari kita bedah berkas-berkas kunci di atas:

  1. Baris 1–2 pada lib.rs (pub mod kitchen; pub mod menu;): dua module domain dideklarasikan publik — keduanya adalah antarmuka resmi crate warung bagi pemakai luar, termasuk binary kita sendiri.
  2. Baris 4 pada lib.rs (mod utils;): module util dideklarasikan tanpa pub. Deklarasi ini menempatkan utils di tree crate library namun tidak mengekspornya — binary dan pemakai eksternal tidak bisa menulis warung::utils::....
  3. Baris 1 pada menu.rs (use crate::utils::format_rupiah;): sesama warga crate library tetap bebas meminjam utilitas internal. Path crate:: di sini merujuk crate library, karena berkas menu.rs adalah bagian dari crate tersebut.
  4. Baris 8–13 pada menu.rs (pub fn menu_favorit()): fungsi penghasil data membangun Vec<MenuItem> — struct dengan field publik yang aman dibaca pemakai.
  5. Baris 1–2 pada main.rs (use warung::kitchen; use warung::menu;): binary mengimpor module library persis seperti mengimpor crate luar. Nama warung pada path itu adalah nama crate library, diambil dari nama package di Cargo.toml.
  6. Baris 5–7 pada main.rs: alur program terbaca sebagai cerita — sambut, tampilkan menu, masak pesanan. Tidak ada satu baris logika bisnis di binary; semuanya didelegasikan ke library.

Hubungan kedua crate dalam package tersebut dapat kita petakan:

Diagram menegaskan arah ketergantungan satu arah: binary bergantung pada library, dan hanya menyentuh module yang sengaja dibuka — panah dari main.rs berhenti di menu dan kitchen, tidak pernah menyentuh utils.

Konvensi Komunitas yang Kita Ikuti #

Struktur di atas bukan gaya bebas — ia mengikuti kebiasaan yang stabil di ekosistem Rust. Daftar periksa singkatnya: nama package di Cargo.toml menjadi nama crate library, jadi pilihlah nama yang enak ditulis setelah use; src/lib.rs berfungsi sebagai crate root yang hanya berisi deklarasi mod dan item pendek yang mewakili identitas crate; module util internal dideklarasikan tanpa pub agar otomatis tersembunyi; binary di src/main.rs menipis menjadi orkestrator; dan unit test ditempatkan di dalam module masing-masing dengan atribut #[cfg(test)] — pola yang kita pakai pada Chapter berikutnya.

Ringkasan #

Struktur idiomatik proyek Rust memisahkan dua peran dalam satu package: crate library dari src/lib.rs membawa seluruh module domain dan util, sedangkan crate binary dari src/main.rs menipis menjadi pemanggil library melalui use warung::.... Module util cukup dideklarasikan privat agar otomatis tidak masuk antarmuka, dan arah ketergantungan yang satu arah menjaga antarmuka tetap disiplin. Struktur ini juga menjadi fondasi pengujian: module test dapat menempel pada tiap module di library, sementara integration test di folder tests/ menguji antarmuka publik persis seperti pemakai nyata. Dengan struktur ini, satu lapisan kontrol masih kurang: bagaimana membuka item untuk sesama crate library tanpa membocorkannya ke pemakai luar? Jawabannya adalah pub(crate) — pembahasan Chapter penutup Section ini.