Default implementation menyelamatkan kita dari menulis perilaku yang sama berulang-ulang, tetapi bawaan yang sama untuk semua orang hanyalah kompromi, bukan jawaban akhir. Ringkasan generik “(Baca selengkapnya dari …)” memang cukup untuk cuitan dan kiriman foto, namun jelas memalukan bagi artikel berita yang justru memiliki kepala berita dan lokasi siap pakai untuk ditampilkan. Kebutuhannya spesifik: tipe tertentu ingin memakai kontrak yang sama namun dengan perilaku berbeda pada method tertentu. Rust menjawab lewat override — cukup tuliskan method yang ingin diganti di dalam blok impl Trait for Type, dan kehadiran method tersebut meniadakan body bawaan untuk tipe itu. Perilaku bawaan tetap berlaku untuk tipe lain yang tidak menuliskannya.

Menimpa Method Bawaan #

Kita kembali ke kontrak Summary dengan bawaan dari chapter sebelumnya, lalu memberi NewsArticle ringkasan versi sendiri — sementara Tweet membiarkan bawaan bekerja:

rust
trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Baca selengkapnya dari {}...)", self.summarize_author())
    }
}

struct NewsArticle {
    author: String,
    location: String,
    headline: String,
}

struct Tweet {
    username: String,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

impl Summary for NewsArticle {
    fn summarize_author(&self) -> String {
        self.author.clone()
    }

    fn summarize(&self) -> String {
        format!("{}, {} ({})", self.headline, self.location, self.author)
    }
}

fn main() {
    let article = NewsArticle {
        author: String::from("Rina Wijaya"),
        location: String::from("Jakarta"),
        headline: String::from("Banjir Rob Melanda Pesisir Utara"),
    };

    let tweet = Tweet {
        username: String::from("budi_perjalanan"),
    };

    println!("Ringkasan artikel: {}", article.summarize());
    println!("Ringkasan cuitan: {}", tweet.summarize());
}

Output yang Dihasilkan:

text
Ringkasan artikel: Banjir Rob Melanda Pesisir Utara, Jakarta (Rina Wijaya)
Ringkasan cuitan: (Baca selengkapnya dari @budi_perjalanan...)

Dua baris output kini memperlihatkan dua jalur berbeda dari satu kontrak yang sama. Mari kita bedah:

  1. Baris 4-6 (fn summarize(&self) -> String { ... } di dalam trait): body bawaan masih tertulis di definisi trait dan tetap menjadi cadangan resmi untuk semua pelaksana kontrak.
  2. Baris 19-23 (impl Summary for Tweet { ... }): blok impl Tweet hanya memuat summarize_author, sama seperti chapter sebelumnya. Karena summarize tidak ditulis, bawaan tetap dipakai — terlihat jelas pada baris output kedua yang masih berpola “(Baca selengkapnya dari …)”.
  3. Baris 25-33 (impl Summary for NewsArticle { ... }): blok impl ini memuat dua method. Keberadaan fn summarize di sini adalah override: satu baris deklarasi cukup untuk membatalkan bawaan bagi NewsArticle.
  4. Baris 31 (format!("{}, {} ({})", self.headline, self.location, self.author)): versi pengganti memanfaatkan field khas NewsArticle — headline, location, author — sehingga ringkasannya informatif. Body bawaan tidak bisa melakukan hal ini karena trait hanya mengenal summarize_author, bukan field-field tersebut.
  5. Baris 46 (println!("Ringkasan artikel: {}", article.summarize());): method resolution bekerja seperti biasa: kompilator mencari summarize untuk NewsArticle, menemukannya di blok impl, dan mengikat pemanggilan ke sana. Bawaan tidak pernah sampai terlibat untuk artikel.

Default Diganti Total, Bukan Dilapisi #

Kesalahpahaman yang paling sering muncul berasal dari kebiasaan pewarisan OOP: diasumsikan override “menumpuk” di atas bawaan sehingga bawaan masih bisa dipanggil dari dalam versi pengganti. Pada Rust, hubungan itu total. Untuk tipe yang menimpa summarize, body bawaan milik trait tidak dijadikan bagian program untuk tipe tersebut — tidak ada sintaks seperti super::summarize() untuk memanggilnya dari dalam override. Ketika NewsArticle menulis versinya sendiri, kode bawaan itu berhenti berlaku untuk NewsArticle seluruhnya.

Konsekuensi desainnya jelas: jika sebagian perilaku bawaan memang ingin dipertahankan, jangan menyalin teksnya ke dalam override — keluarkan bagian itu menjadi method terpisah. Pola summarize_author pada contoh kita memang sengaja dibangun demikian: bagian yang bawaan maupun override sama-sama perlukan (nama pembuat) dijadikan method tersendiri, sehingga keduanya memanggil summarize_author alih-alih menggandakan teks. Kontrak menjadi lapisan komponen, bukan gudang teks untuk disalin.

Kedua, perhatikan bahwa override berlaku per-tipe, bukan per-trait. Keputusan NewsArticle menimpa summarize tidak mengubah apa pun bagi Tweet, dan tipe ketiga yang mendaftar besok bebas memilih jalur mana pun. Bawaan tetap utuh di definisi trait sebagai titik awal baku yang bisa ditolak tipe demi tipe.

Urutan keputusan kompilator dapat kita rumuskan menjadi dua langkah yang selalu sama: saat method resolution mencari summarize untuk tipe T, blok impl Summary for T dicari lebih dulu; jika method tersebut ditemukan di sana, pemanggilan diikat ke implementasi tipe. Jika tidak ditemukan, pencarian berlanjut ke body bawaan pada definisi trait. Dua langkah inilah seluruh aturan mainnya — tidak ada mekanisme ketiga, tidak ada penimbangan lain. Konsekuensi desain yang mengikuti aturan sederhana ini juga jauh: menambahkan default pada trait yang sudah beredar tidak pernah mematahkan tipe yang sudah menimpa method tersebut, karena implementasi milik tipe selalu menang; sebaliknya, menambahkan default baru justru menyelamatkan tipe yang belum menulis method itu dari E0046.

Pedoman Memilih Default atau Override #

Pengalaman menata kontrak biasanya mengerucut pada empat kebiasaan berikut:

  • Mulai dari bawaan. Tipe baru yang mengimplementasikan trait cukup memenuhi method wajib dan langsung jalan dengan bawaan; override ditulis belakangan hanya saat kebutuhan nyata muncul.
  • Tinpa ketika data tipe mengizinkan sesuatu yang jauh lebih baik. NewsArticle menimpa karena kepala berita dan lokasi menghasilkan ringkasan yang jauh lebih bernilai daripada pola generik.
  • Jangan tinpa hanya demi mempercantik. Override yang isinya sama dengan bawaan hanya menambah teks tanpa menambah perilaku — duplikasi yang persis ingin kita hindarkan sejak chapter pertama.
  • Pindahkan bagian bersama ke method wajib. Jika bawaan dan override sama-sama membutuhkan potongan perilaku yang sama, angkat potongan itu menjadi method tersendiri di kontrak sehingga keduanya memanggilnya.

Sebagai pemeriksaan akhir sebelum menulis override, tanyakan satu hal: apakah pemanggil yang membaca output untuk tipe ini akan mengharapkan bentuk yang berbeda dari bawaan? Jika jawabannya tidak, biarkan bawaan bekerja — kode yang tidak ditulis adalah kode yang tidak perlu dirawat.

Ringkasan #

Override adalah cara tipe menyatakan “kontrak ini tetap saya ikuti, tetapi method ini saya kerjakan sendiri”: cukup tuliskan method bersangkutan di blok impl Trait for Type, dan kehadirannya meniadakan body bawaan secara total untuk tipe tersebut — tanpa tumpang-tindih, tanpa panggilan ke bawaan, dan tanpa efek pada tipe lain. Pedoman praktisnya sederhana: mulai dari bawaan, tinpa hanya ketika data tipe menghasilkan perilaku yang jauh lebih baik, dan angkat bagian bersama menjadi method wajib agar tidak ada teks yang digandakan. Kontrak kini hidup dengan tiga lapis sekaligus: definisi, implementasi, dan bawaan yang bisa ditimpa. Berikutnya kita mengangkat pandangan ke fungsi: bagaimana fungsi biasa menerima tipe apa pun yang memenuhi kontrak?