Builder pattern pada chapter sebelumnya sudah mengunci nilai: konfigurasi tidak sah tidak bisa lahir. Namun builder masih menyisakan satu kebebasan berbahaya — urutan pemanggilan. Banyak API memiliki urutan wajib: koneksi harus authenticate sebelum query, pembayaran harus authorize sebelum capture, dan mesin kopi harus menyeduh sebelum menuang. Penjagaan urutan secara konvensional dilakukan dengan flag status internal yang diperiksa di setiap method, lalu panic atau error ketika urutannya salah. Typestate pattern menempatkan penjagaan tersebut pada sistem tipe: setiap state menjadi tipe tersendiri, method hanya tersedia pada tipe state yang tepat, dan perpindahan state terjadi lewat ownership move. Hasilnya, pemanggilan yang tidak berurutan tidak lagi ditolak oleh pemeriksaan runtime, melainkan oleh compiler dengan error E0599 sebelum program sempat berjalan.
Masalah: Urutan Pemanggilan yang Bebas #
Bayangkan mesin kopi dengan API bebas: new(), start_brew(), finish(), dan pour(). Tidak ada satu pun pada tanda tangan keempat method tersebut yang menyatakan urutan. Pemanggil bisa memanggil pour() langsung setelah new() — menumpahkan cangkir kosong, atau lebih buruk, memanggil start_brew() dua kali dan menggiling biji dua kali lipat. Penjagaan konvensional menambahkan field status seperti enum MachineState { Idle, Brewing, Ready } di dalam struct, lalu setiap method membuka status dan menolak pemanggilan yang salah.
Pendekatan status internal bekerja, tetapi membayar tiga kali. Setiap method membayar pemeriksaan tambahan, setiap penolakan harus dipilih antara panic atau Result yang menggandakan tanda tangan, dan kesalahan urutan tetap hanya ketahuan saat program berjalan. Pada API yang dipakai banyak orang, bug urutan seperti “menuang sebelum menyeduh” akan ditemukan pemanggil pertama yang tidak membaca dokumentasi dengan teliti.
State sebagai Tipe #
Typestate mengangkat state dari field internal menjadi tipe terpisah. Mesin kopi kini punya tiga tipe: IdleMachine, BrewingMachine, dan ReadyMachine. Method yang sah hanya diimplementasikan pada tipe state yang tepat, dan setiap method mengonsumsi self lalu mengembalikan tipe state berikutnya. Karena nilai lama sudah berpindah (move) ke dalam method, pemanggil tidak bisa menahan state lama sambil memegang state baru.
| State saat ini | Method yang tersedia | State berikutnya |
|---|---|---|
IdleMachine | new, start_brew | BrewingMachine |
BrewingMachine | finish | ReadyMachine |
ReadyMachine | pour | nilai u32 (cangkir) |
Mari kita tulis program lengkapnya:
pub struct IdleMachine {
beans_grams: u32,
}
pub struct BrewingMachine {
beans_grams: u32,
seconds_left: u32,
}
pub struct ReadyMachine {
cups: u32,
}
impl IdleMachine {
pub fn new(beans_grams: u32) -> Self {
Self { beans_grams }
}
// Hanya mesin idle yang boleh mulai menyeduh
pub fn start_brew(self) -> BrewingMachine {
println!("Menggiling {} gram biji kopi...", self.beans_grams);
BrewingMachine {
beans_grams: self.beans_grams,
seconds_left: 30,
}
}
}
impl BrewingMachine {
// Hanya mesin yang sedang menyeduh yang boleh menunggu selesai
pub fn finish(self) -> ReadyMachine {
println!("Menyeduh {} gram kopi selama {} detik...", self.beans_grams, self.seconds_left);
ReadyMachine { cups: 2 }
}
}
impl ReadyMachine {
// Hanya mesin dengan kopi siap yang boleh menuang
pub fn pour(self) -> u32 {
println!("Menuang {} cangkir kopi", self.cups);
self.cups
}
}
fn main() {
let machine = IdleMachine::new(18);
let ready = machine.start_brew().finish();
let cups = ready.pour();
println!("Total cangkir yang dinikmati: {}", cups);
}Output yang Dihasilkan:
Menggiling 18 gram biji kopi...
Menyeduh 18 gram kopi selama 30 detik...
Menuang 2 cangkir kopi
Total cangkir yang dinikmati: 2Mari kita bedah kode di atas:
- Baris 1-12 (
struct IdleMachine,BrewingMachine,ReadyMachine): tiga state menjadi tiga tipe berdiri sendiri. Setiap tipe hanya menyimpan data yang relevan pada state tersebut — misalnyaseconds_lefthanya ada padaBrewingMachine, karena hitungan waktu barulah bermakna saat menyeduh. - Baris 15-17 (
pub fn new(beans_grams: u32) -> Self): titik masuk tunggal menghasilkanIdleMachine. Tipe hasilnew()sendiri sudah mengomunikasikan kontrak: mesin baru selalu dimulai dari keadaan idle. - Baris 20 (
pub fn start_brew(self) -> BrewingMachine): method perpindahan mengambilselfsecara nilai, bukan&selfatau&mut self. Setelah pemanggilan, nilaiIdleMachinelama tidak bisa dipakai lagi karena kepemilikannya sudah berpindah ke dalam method — mesin yang sama tidak bisa mulai menyeduh dua kali. - Baris 22-25 (
BrewingMachine { beans_grams: self.beans_grams, seconds_left: 30 }): method membongkar data dari state lama dan membentuk state baru. Perpindahan state dan perpindahan kepemilikan terjadi dalam satu tindakan; tidak ada momen ketika kedua state hidup bersamaan. - Baris 31 (
pub fn finish(self) -> ReadyMachine): pola yang sama berulang dariBrewingMachinekeReadyMachine. Perhatikan bahwafinishhanya diimplementasikan untukBrewingMachine— tidak adafinishpadaIdleMachine, jadi pemanggilan “menyelesaikan seduhan yang belum dimulai” bukan sekadar gagal saat dijalankan; pemanggilan tersebut tidak pernah ada sebagai method yang bisa ditulis. - Baris 39 (
pub fn pour(self) -> u32): state akhir menuang dan mengembalikan hasil sebagai nilai biasa. Setelahpour, seluruh mesin habis terkonsumsi — sesuai domain: setelah kopi dituang, mesin kembali ke titik awal lewatIdleMachine::newuntuk siklus berikutnya. - Baris 46-49 (
fn main()): rantaimachine.start_brew().finish()bekerja karena tipe kembalianstart_brew()adalahBrewingMachine, danfinishtersedia tepat pada tipe tersebut. Tipe menjadi penunjuk arah: compiler tahu method mana yang sah setelah setiap langkah.
Pemanggilan Tak Berurutan Gagal di Kompilasi #
Sekarang kita uji janji utamanya. Program berikut sama seperti program di atas, tetapi main-nya memanggil pour() pada mesin yang masih idle. Simpan sebagai main.rs, lalu jalankan rustc --edition 2021 main.rs:
pub struct IdleMachine {
beans_grams: u32,
}
pub struct BrewingMachine {
beans_grams: u32,
seconds_left: u32,
}
pub struct ReadyMachine {
cups: u32,
}
impl IdleMachine {
pub fn new(beans_grams: u32) -> Self {
Self { beans_grams }
}
pub fn start_brew(self) -> BrewingMachine {
println!("Menggiling {} gram biji kopi...", self.beans_grams);
BrewingMachine {
beans_grams: self.beans_grams,
seconds_left: 30,
}
}
}
impl BrewingMachine {
pub fn finish(self) -> ReadyMachine {
println!("Menyeduh {} gram kopi selama {} detik...", self.beans_grams, self.seconds_left);
ReadyMachine { cups: 2 }
}
}
impl ReadyMachine {
pub fn pour(self) -> u32 {
println!("Menuang {} cangkir kopi", self.cups);
self.cups
}
}
fn main() {
let machine = IdleMachine::new(18);
// Kesalahan: menuang padahal mesin masih idle
let cups = machine.pour();
println!("Total cangkir yang dinikmati: {}", cups);
}Pesan error kompilator:
error[E0599]: no method named `pour` found for struct `IdleMachine` in the current scope
--> main.rs:45:24
|
1 | pub struct IdleMachine {
| ---------------------- method `pour` not found for this struct
...
45 | let cups = machine.pour();
| ^^^^ method not found in `IdleMachine`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0599`.Error E0599 menyatakan fakta yang sederhana namun tegas: method bernama pour tidak ditemukan pada IdleMachine. Compiler tidak memahami maksud pemanggil; compiler hanya melihat bahwa machine bertipe IdleMachine, dan satu-satunya method pengguna pada tipe tersebut adalah start_brew. Untuk bisa memanggil pour, pemanggil wajib melewati start_brew() dan finish() terlebih dahulu — persis urutan yang domain mesin kopi tuntukan. Perhatikan juga bahwa penjagaan ini tidak menambah satu pun pemeriksaan runtime: tidak ada flag status, tidak ada panic, dan method-methodnya tetap murni tanpa Result.
Varian Satu Struct Generik #
Tiga struct terpisah menduplikasi field beans_grams pada contoh kita. Untuk state machine yang lebih besar, penulis API sering memilih satu struct generik CoffeeMachine<State> dengan field data yang sama sekali tidak berubah, lalu menandai state lewat type parameter nol biaya seperti PhantomData<State>. Method per state kemudian ditulis sebagai impl CoffeeMachine<Idle>, impl CoffeeMachine<Brewing>, dan seterusnya — kontrak urutannya identik dengan versi tiga struct, tetapi data tinggal dideklarasikan satu kali. Kita akan bertemu kembali pola PhantomData pada section Tipe Lanjutan, jadi contoh pada chapter ini sengaja memakai bentuk tiga struct yang paling eksplisit.
Kapan Typestate Layak Dipakai #
Typestate membayar boilerplate: tiga tipe untuk tiga state, dan setiap perpindahan menuntut method tersendiri. Pembayarannya sepadan ketika urutan pemanggilan adalah bagian dari kontrak yang mahal dilanggar — protokol jaringan, siklus hidup transaksi, atau setup resource seperti koneksi yang harus connect lalu authenticate sebelum query. Typestate juga cocok digabungkan dengan builder: builder dari chapter sebelumnya menegakkan nilai, typestate menegakkan urutan, dan keduanya sama-sama berujung pada compile-time guarantee tanpa pemeriksaan runtime tambahan.
Untuk kasus kecil sebaliknya, satu method dengan enum status dan match yang rapi sudah memadai. Aturan praktisnya: jika kesalahan urutan hanya menghasilkan kebingungan kecil, jangan bayar harga typestate; jika kesalahan urutan mengirim pembayaran dua kali atau merusak protokol, biarkan compiler yang memegang kuncinya.
Ringkasan #
Typestate pattern mengubah state machine runtime menjadi struktur tipe: setiap state menjadi tipe, method hanya diimplementasikan pada tipe yang tepat, dan perpindahan state dilakukan method yang mengonsumsi self dan mengembalikan tipe berikutnya. Ownership move memastikan state lama mati begitu perpindahan terjadi, sehingga urutan pemanggilan menjadi tunggal. Pemanggilan yang melanggar urutan ditolak dengan E0599 — method tidak ditemukan pada tipe state tersebut — jauh sebelum program berjalan. Teknik kunci di baliknya, yaitu method yang mengambil self secara nilai, ternyata berguna jauh melampaui state machine; chapter berikutnya mengangkat consuming method tersebut menjadi alat desain API yang berdiri sendiri.