32 poin oleh GN⁺ 2025-12-07 | 1 komentar | Bagikan ke WhatsApp
  • Memperkenalkan kebiasaan menulis kode untuk mencegah bug sejak awal dengan memanfaatkan secara aktif sistem tipe dan compiler Rust
  • Menunjukkan contoh bau kode (Code Smell) yang rapuh seperti pengindeksan vektor, penyalahgunaan Default, match yang tidak lengkap, dan parameter boolean yang tidak perlu, serta menjelaskan alternatifnya
  • Prinsip utamanya adalah merancang struktur agar compiler memaksa invarian, dengan memanfaatkan pattern matching, field privat, dan atribut #[must_use]
  • Menyajikan secara konkret teknik defensif di tingkat kode nyata seperti penggunaan TryFrom, pembongkaran struct secara lengkap, mutabilitas sementara, dan validasi di konstruktor
  • Pola-pola ini penting untuk menjaga stabilitas saat refactoring dan meningkatkan maintainability jangka panjang

Gambaran umum pemrograman defensif

  • Titik yang diberi komentar // this should never happen adalah lokasi ketika invarian implisit rusak
    • Dalam banyak kasus, pengembang tidak mempertimbangkan semua kondisi batas atau perubahan kode di masa depan
  • Compiler Rust menjamin keamanan memori, tetapi kesalahan logika bisnis tetap bisa terjadi
  • Pola kebiasaan kecil (idiom) yang diperoleh dari pengalaman kerja bertahun-tahun dapat sangat meningkatkan kualitas kode

Code Smell: pengindeksan vektor

  • Bentuk if !vec.is_empty() { let x = &vec[0]; } memiliki risiko panic saat runtime karena pemeriksaan panjang dan pengindeksan terpisah
  • Dengan menggunakan pattern matching pada slice (match vec.as_slice()), compiler memaksa pemeriksaan semua keadaan
    • Semua kasus seperti vektor kosong, satu elemen, atau elemen duplikat dapat ditangani secara eksplisit
  • Ini adalah contoh representatif dari desain yang membuat compiler menjamin invarian

Code Smell: penggunaan Default yang sembarangan

  • ..Default::default() menimbulkan masalah risiko field terlewat saat field baru ditambahkan dan penetapan nilai implisit
  • Jika semua field diinisialisasi secara eksplisit, compiler akan memaksa field baru untuk ikut diatur
  • Dengan bentuk let Foo { field1, field2, .. } = Foo::default();, dimungkinkan membongkar struct default lalu menimpa sebagian field secara selektif
    • Ini menjaga keseimbangan antara mempertahankan nilai default dan override yang eksplisit
    Iklan

Code Smell: implementasi trait yang rapuh

  • Saat membandingkan dengan membongkar seluruh field struct, penambahan field baru akan memunculkan error kompilasi sebagai peringatan
    • Contoh: saat mengimplementasikan PartialEq, gunakan let Self { size, toppings, .. } = self;
  • Jika field baru seperti extra_cheese ditambahkan, logika perbandingan dipaksa untuk ditinjau ulang
  • Prinsip yang sama juga dapat diterapkan pada trait lain seperti Hash, Debug, dan Clone

Code Smell: perlu TryFrom alih-alih From

  • Jika konversi tidak selalu berhasil, gunakan TryFrom untuk menyatakan kemungkinan kegagalan alih-alih From
  • Penggunaan unwrap_or_else adalah sinyal bahwa kegagalan potensial sedang disembunyikan, dan pendekatan fail fast lebih aman

Code Smell: match yang tidak lengkap

  • Pola catch-all seperti _ => {} berisiko membuat kasus baru terlewat saat variant baru ditambahkan
  • Jika semua variant dicantumkan secara eksplisit, compiler akan memperingatkan ketika ada kasus baru yang belum ditangani
  • Logika yang sama dapat dikelompokkan dalam bentuk Variant3 | Variant4

Code Smell: penyalahgunaan placeholder _

  • Jika hanya menggunakan _, tidak jelas variabel mana yang dihilangkan
  • Nama eksplisit seperti has_fuel: _, has_crew: _ meningkatkan keterbacaan

Pattern: mutabilitas sementara (Temporary Mutability)

  • Saat data hanya perlu mutable selama inisialisasi, gunakan bentuk let mut data = ...; data.sort(); let data = data;
  • Dengan memanfaatkan block scope, variabel sementara tidak terekspos ke luar
    • Contoh: let data = { let mut d = get_vec(); d.sort(); d };
    Iklan
  • Ini memungkinkan pemisahan cakupan yang jelas dalam proses inisialisasi yang memakai beberapa variabel sementara

Pattern: memaksa validasi konstruktor

  • Saat membuat struct, pastikan logika validasi wajib dilalui
    • Jika field _private: () ditambahkan, pembuatan langsung dari luar menjadi tidak mungkin
    • Atribut #[non_exhaustive] mencegah konstruksi dari luar crate dan memberi sinyal adanya perluasan di masa depan
  • Jika ingin memaksanya juga di modul internal, gunakan struktur modul bertingkat dengan tipe privat (Seal)
    • Karena Seal hanya ada di internal, pembuatan langsung selain melalui new() menjadi tidak mungkin
  • Dengan menjadikan field privat dan menyediakan getter, keadaan immutable dapat dipertahankan
  • Kriteria penerapan
    • Memblokir kode eksternal: _private atau #[non_exhaustive]
    • Memblokir kode internal: modul privat + Seal
    • Mengubah logika validasi menjadi jaminan di tingkat compiler

Pattern: memanfaatkan atribut #[must_use]

  • #[must_use] mencegah nilai return penting diabaikan
    • Contoh: #[must_use = "Configuration must be applied to take effect"]
    Iklan
  • Jika pengguna mengabaikan nilai return, compiler akan mengeluarkan peringatan
  • Ini adalah mekanisme defensif yang sederhana tetapi kuat, dan juga banyak dipakai di standard library seperti Result

Code Smell: parameter boolean

  • Bentuk fn process_data(..., compress: bool, encrypt: bool, validate: bool) memiliki makna yang tidak jelas dan risiko salah urutan
  • Gunakan enum Compression, enum Encryption, dan sebagainya untuk mengekspresikan maksud secara eksplisit
  • Jika ada banyak opsi, gunakan parameter struct (Params struct)
    • Metode preset seperti ProcessDataParams::production() meningkatkan reusabilitas
  • Saat opsi baru ditambahkan, dampak pada titik pemanggilan yang ada dapat diminimalkan

Otomatisasi dengan Clippy lints

  • Pola defensif utama dapat diperiksa secara otomatis dengan lint Clippy
    • indexing_slicing: melarang pengindeksan langsung
    • fallible_impl_from: menyarankan TryFrom alih-alih From
    • wildcard_enum_match_arm: melarang pola _
    • fn_params_excessive_bools: peringatan untuk parameter boolean yang terlalu banyak
    • must_use_candidate: menyarankan kandidat #[must_use]
  • Dapat diterapkan ke seluruh proyek melalui #![deny(clippy::...)] atau pengaturan di Cargo.toml

Kesimpulan

  • Inti pemrograman defensif adalah memanfaatkan sistem tipe dan compiler Rust secara aktif untuk membuat invarian menjadi eksplisit dan dapat diverifikasi
  • Pola-pola ini membantu menjaga stabilitas saat refactoring, meminimalkan kemungkinan bug, dan memperkuat maintainability jangka panjang
  • Ini adalah pendekatan yang menjalankan prinsip: “bug yang tidak bisa dikompilasi adalah bug terbaik”

1 komentar

 
GN⁺ 2025-12-07
Komentar Hacker News
  • Tulisannya bagus. Namun, contoh PizzaOrder terasa seperti memasukkan terlalu banyak concern ke dalam satu struct
    Jika tujuannya adalah mengecualikan ordered_at dari perbandingan, menurut saya lebih baik memisahkannya menjadi dua struct: PizzaDetails dan PizzaOrder
    Dengan begitu, saat mengimplementasikan PartialEq, bisa dibuat jelas bahwa yang dibandingkan hanya details

    • Poin yang bagus. Tapi secara logis saya tetap menganggap ini pemodelan yang keliru
      Jika waktu pemesanan berbeda, maka itu bukan pesanan yang sama, jadi mendefinisikannya setara di level tipe itu berbahaya
      Menaruh PartialEq pada PizzaDetails tidak masalah, tetapi logika perbandingan pesanan seharusnya diletakkan di fungsi bisnis yang terpisah
    • Pendekatan memisahkan struktur memang bagus, tetapi masalahnya adalah saat PizzaDetails diubah, perubahan itu bisa memengaruhi logika deduplikasi pizza
      Idealnya, struct hanya digunakan untuk mengelompokkan data
      Agar perubahan tidak berdampak ke tempat lain, bisa juga dipertimbangkan tipe terpisah seperti PizzaComparator atau PizzaFlavor
      Akan bagus jika, seperti di Protobuf, kita bisa memberi anotasi field seperti {important_to_flavour=true}
    • Memisahkan struktur hanya demi cara perbandingan yang berbeda bukanlah sesuatu yang bisa digeneralisasi
      Misalnya, bagaimana pemisahannya jika kita ingin membandingkan string tanpa membedakan huruf besar-kecil?
  • Salah satu hal yang sangat keren di Rust adalah sering kali pemrograman defensif tidak diperlukan
    Berkat aturan ownership dan reference, kita bisa mendapat jaminan bahwa akses ke objek tertentu itu unik di seluruh program
    Reference tidak bisa bernilai null, dan smart pointer juga tidak bisa bernilai null
    Sistem tipe juga menjamin bahwa setelah ownership self dipindahkan, pemanggilan metode berikutnya tidak mungkin dilakukan
    Karena itu, keamanan thread, lifetime, kemampuan cloning, dan sebagainya dapat diverifikasi secara global saat waktu kompilasi

    • Saya juga merasa keunggulan sejati Rust ada pada hal-hal yang “tidak perlu dipikirkan”
      Di bahasa lain, manfaat yang didapat dengan menjaga immutability lewat gaya fungsional dipaksakan oleh Rust melalui sistem tipe
    • Tapi komentar ini tampaknya tidak terlalu terkait dengan artikel aslinya
      Topik artikelnya adalah bug logis yang tidak tertangkap bahkan oleh borrow checker
    • Isi artikel lebih banyak berfokus pada pola penulisan kode untuk menghindari kesalahan logis saat program diperbaiki secara berulang
  • Rasanya bijak untuk menghindari indexing langsung ke array atau vector
    Pada hari terjadinya insiden unwrap Cloudflare, saya juga menemukan bug ketika slice melewati akhir vector
    Setelah itu saya beralih ke pendekatan berbasis iterator dan rasanya jauh lebih aman

    • Saya tidak merasa insiden unwrap itu perlu dianggap sebagai “insiden”
      unwrap di Rust sama seperti assert di C. Jika gagal, itu hanya berfungsi memberi tahu adanya masalah
      Di Rust pun bug tetap bisa ditulis
    • Pada akhirnya ini masalah yang sama. Kubu Rust sering berkata mari tinggalkan C, tetapi di C pun umum untuk memakai handle alih-alih indeks
  • Salah satu kebiasaan yang perlu diwaspadai pengembang Rust adalah menambahkan dependensi crate yang tidak perlu
    Rust cenderung mendorong kebiasaan seperti ini. Misalnya, penggunaan crate rand sebagai contoh dasar di Rust Book ikut menciptakan suasana semacam itu
    Tentu ini adalah pilihan strategis agar paket terkait kriptografi bisa diganti dengan mudah, tetapi tetap saja menjadi kebiasaan adalah masalah

    • Saya juga sempat merasa enggan terhadap Rust karena contoh itu
      Tapi belakangan saya memahami maksudnya dan pandangan saya berubah
  • Implementasi partial equality itu menarik
    Hal lain yang membuat saya penasaran adalah cara memakai enum saat menghindari parameter boolean
    Saya biasanya memakai struct pembungkus bool, tetapi sayang tidak bisa diperlakukan seperti bool biasa
    Saya jadi penasaran apakah ada cara memakai enum seperti bool

    • Saya juga hampir selalu lebih suka enum + match!
      Logika yang diperlukan saya bungkus dalam Trait, atau saya tambahkan metode bersama di blok impl <Enum>
      Dengan begitu, keterbacaan lebih baik dan perilaku tiap anggota bisa didefinisikan dengan jelas
    • Mungkin bisa mencoba sesuatu seperti impl Deref, tetapi saya tidak yakin itu ide yang bagus
  • Pernyataan match pada contoh pertama terasa berlebihan
    Vec.first() atau Vec.iter().nth(0) lebih jelas dan lebih sesuai dengan maksudnya

    • Saya juga setuju. Memakai match justru menjadi solusi yang lebih rumit daripada masalahnya
      Jika if bisa dihapus, maka match juga bisa dihapus, jadi tidak ada perbedaan dari sisi keamanan
      first() jauh lebih ringkas dan jelas
    • Untuk mengekspresikan perilaku yang sama dengan lebih sederhana, bisa juga memakai exactly_one milik itertools
    • Namun, match tetap bermakna karena mendorong kita untuk juga menangani kasus “saat ada satu atau lebih elemen”
      Dengan kata lain, ini menegaskan prinsip hindari pemisahan antara pemeriksaan dan kode yang bergantung padanya
  • Setiap kali membaca tulisan seperti ini, saya jadi bertanya-tanya kenapa tidak ada tim khusus yang memantau pola kode
    Akan bagus jika ada tim yang, seperti SOC atau QA, mengamati pola dalam codebase dalam jangka panjang
    Alat pendeteksi code smell otomatis punya keterbatasan

    • Di perusahaan kami (sekitar 300 orang), ada tim khusus utang teknis dengan peran seperti ini
      Mereka menangani aturan lint, dokumentasi, pelatihan pengembang, dan pemeliharaan pustaka bersama
      Jika banyak tim mengulangi masalah yang sama, mereka merancang API inti yang bisa menyatukannya
    • Di perusahaan teknologi besar, kebanyakan memang punya tim seperti itu
      Hanya saja, jika kodenya sudah mencapai jutaan baris, kenyataannya pengelolaannya sangat sulit
  • Saya sedang memikirkan bagaimana mendorong pola penulisan kode yang baik seperti ini di dalam tim
    Saat code review, ini sering berkembang menjadi “perdebatan gaya” dan menjadi tidak produktif
    Tapi anehnya, ketika linter mengeluarkan peringatan, perdebatan seperti itu hampir hilang

  • Penambahan trait TryFrom di versi 1.34 benar-benar sangat berguna
    Bisa jadi kode yang memakai unwrap_or_else() adalah sisa dari masa sebelum itu
    Dokumentasi trait From sekarang menjelaskan dengan sangat jelas kapan trait itu sebaiknya diimplementasikan

    • Saya masih belajar Rust, dan nama unwrap_or_else() terdengar lucu bagi saya, seolah seperti “memerintah komputer sambil mengancamnya”
  • Saya rasa pola pemrograman defensif seperti ini juga akan membantu meningkatkan kualitas pembuatan kode AI skala besar
    Umpan balik konkret yang diberikan Clippy dan compiler Rust bisa berperan besar membantu agen AI mengurangi kesalahan dan menemukan arah yang tepat