Default and the builder pattern
Anda bisa mengimplementasikan trait Default
untuk memberi value ke struct
atau enum
yang menurut Anda paling umum (paling sering) digunakan. Builder pattern berfungsi dengan baik dengan menggunakan Default
ini, agar pengguna dengan mudah membuat perubahan saat mereka mau. Pertama-tama, kita lihat terlebih dahulu apa itu Default
. Sebenarnya, hampir semua type di Rust telah memiliki Default
. Contohnya: 0, "" (empty strings), false
, dll.
fn main() { let default_i8: i8 = Default::default(); let default_str: String = Default::default(); let default_bool: bool = Default::default(); println!("'{}', '{}', '{}'", default_i8, default_str, default_bool); }
Hasil printnya adalah '0', '', 'false'
.
Jadinya, Default
mirip seperti function new
, namun Anda tidak memberikan input apapun. Pertama, kita akan membuat sebuah struct
yang belum mengimplementasikan Default
. Ia memiliki function new
yang mana kita gunakan untuk membuat karakter bernama Billy dengan beberapa status.
struct Character { name: String, age: u8, height: u32, weight: u32, lifestate: LifeState, } enum LifeState { Alive, Dead, NeverAlive, Uncertain } impl Character { fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) -> Self { Self { name, age, height, weight, lifestate: if alive { LifeState::Alive } else { LifeState::Dead }, } } } fn main() { let character_1 = Character::new("Billy".to_string(), 15, 170, 70, true); }
Tapi, mungkin di dunia yang kita ciptakan ini kita menginginkan hampir semua karakternya bernama Billy, berusia 15, tinggi 170, berat 70, dan berstatus alive. Kita bisa mengimplementasikan Default
sehingga kita bisa menuliskan Character::default()
. Codenya terlihat seperti berikut:
#[derive(Debug)] struct Character { name: String, age: u8, height: u32, weight: u32, lifestate: LifeState, } #[derive(Debug)] enum LifeState { Alive, Dead, NeverAlive, Uncertain, } impl Character { fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) -> Self { Self { name, age, height, weight, lifestate: if alive { LifeState::Alive } else { LifeState::Dead }, } } } impl Default for Character { fn default() -> Self { Self { name: "Billy".to_string(), age: 15, height: 170, weight: 70, lifestate: LifeState::Alive, } } } fn main() { let character_1 = Character::default(); println!( "The character {:?} is {:?} years old.", character_1.name, character_1.age ); }
Hasil printnya adalah The character "Billy" is 15 years old.
Jauh lebih mudah!
Sekarang kita bahas builder pattern. Nantinya, kita akan memiliki banyak Billy, jadinya kita akan tetap menyimpan default valuenya. Tetapi banyak karakter lain yang hanya sedikit berbeda statnya. Builder pattern memungkinkan kita untuk melakukan chain menggunakan method-method sederhana untuk mengubah satu value. Ini adalah salah satu method yang dibuat untuk Character
:
#![allow(unused)] fn main() { fn height(mut self, height: u32) -> Self { // 🚧 self.height = height; self } }
Perhatikan pula, bahwa untuk melakukan hal ini kita memerlukan mut self
. Kita sudah melihatnya sekali sebelumnya, dan ini bukanlah mutable reference (&mut self
). Ia akan mengambil ownership dari Self
dan dengan mut
ia akan menjadi mutable, meskipun sebelumnya ia bukan mutable. Ini dikarenakan .height()
memiliki full ownership (kepemilikan penuh) dan tidak ada siapapun yang bisa menyentuhnya, sehingga ia safe untuk menjadi mutable. Kemudian, ia hanya mengubah self.height
dan mengembalikan Self
(yang mana adalah Character
).
Jadi, mari kita buat 3 buah builder method untuk Character. Ketiganya kurang lebih mirip satu sama lainnya:
#![allow(unused)] fn main() { fn height(mut self, height: u32) -> Self { // 🚧 self.height = height; self } fn weight(mut self, weight: u32) -> Self { self.weight = weight; self } fn name(mut self, name: &str) -> Self { self.name = name.to_string(); self } }
Setiap satu method tersebut mengubah satu variabel dan mengembalikan Self
: inilah apa yang kita lihat pada builder pattern. Jadi sekarang kita bisa menulis seperti ini untuk membuat karakter : let character_1 = Character::default().height(180).weight(60).name("Bobby");
. Jika Anda sedang membuat library untuk digunakan oleh orang lain, ini akan memudahkan mereka. Ini sangatlah mudah untuk dipahami oleh pengguna lainnya, karena codenya bisa dipahami persis seperti kalimat berikut : "Berikan aku karakter default, tetapi dengan tinggi 180, berat 60, dan namanya adalah Bobby ." Sejauh ini, codenya akan menjadi seperti berikut:
#[derive(Debug)] struct Character { name: String, age: u8, height: u32, weight: u32, lifestate: LifeState, } #[derive(Debug)] enum LifeState { Alive, Dead, NeverAlive, Uncertain, } impl Character { fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) -> Self { Self { name, age, height, weight, lifestate: if alive { LifeState::Alive } else { LifeState::Dead }, } } fn height(mut self, height: u32) -> Self { self.height = height; self } fn weight(mut self, weight: u32) -> Self { self.weight = weight; self } fn name(mut self, name: &str) -> Self { self.name = name.to_string(); self } } impl Default for Character { fn default() -> Self { Self { name: "Billy".to_string(), age: 15, height: 170, weight: 70, lifestate: LifeState::Alive, } } } fn main() { let character_1 = Character::default().height(180).weight(60).name("Bobby"); println!("{:?}", character_1); }
Method terakhir yang ditambahkan biasanya disebut .build()
. Method ini adalah semacam final check / pemeriksaan terakhir. Di saat Anda memberikan user sebuah method seperti .height()
, Anda bisa memastikan bahwa mereka hanya memasukkan data yang bertype u32()
, tapi bagaimana jika mereka memasukkan 5000 untuk tinggi karakternya? Tentu saja itu bukanlah hal yang baik untuk game yang Anda buat. Kita akan menggunakan method terakhir bernama .build()
yang mengembalikan Result
. Di dalam method tersebut, kita akan memeriksa apakah inputan dari user sudah benar, dan jika memang sudah benar, maka kita akan mengembalikan Ok(Self)
.
Pertama, kita ubah terlebih dahulu method .new()
. Kita tidak ingin user bebas membuat karakter apapun. Jadi kita akan memindahkan value dari impl Default
ke .new()
. Dan sekarang .new()
tidak lagi mengambil inputan apapun.
#![allow(unused)] fn main() { fn new() -> Self { // 🚧 Self { name: "Billy".to_string(), age: 15, height: 170, weight: 70, lifestate: LifeState::Alive, } } }
Ini berarti kita tidak lagi memerlukan impl Default
, karena .new()
telah memiliki semua default value. Jadinya kita bisa menghapus impl Default
.
Sekarang codenya menjadi seperti ini:
#[derive(Debug)] struct Character { name: String, age: u8, height: u32, weight: u32, lifestate: LifeState, } #[derive(Debug)] enum LifeState { Alive, Dead, NeverAlive, Uncertain, } impl Character { fn new() -> Self { Self { name: "Billy".to_string(), age: 15, height: 170, weight: 70, lifestate: LifeState::Alive, } } fn height(mut self, height: u32) -> Self { self.height = height; self } fn weight(mut self, weight: u32) -> Self { self.weight = weight; self } fn name(mut self, name: &str) -> Self { self.name = name.to_string(); self } } fn main() { let character_1 = Character::new().height(180).weight(60).name("Bobby"); println!("{:?}", character_1); }
Tentunya hasilnya pun akan sama: Character { name: "Bobby", age: 15, height: 180, weight: 60, lifestate: Alive }
.
Kita hampir siap untuk membuat method .build()
, tapi masih ada satu problem: bagaimana caranya kita mendorong user untuk menggunakan method tersebut? Sekarang user bisa menuliskan let x = Character::new().height(76767);
dan mendapatkan Character
. Ada banyak cara untuk membuat (memaksa) user nantinya menggunakan method tersebut, dan mungkin Anda bisa membayangkan cara Anda sendiri. Tapi, disini kita akan menggunakan suatu cara, yaitu menambahkan value can_use: bool
ke Character
.
#![allow(unused)] fn main() { #[derive(Debug)] // 🚧 struct Character { name: String, age: u8, height: u32, weight: u32, lifestate: LifeState, can_use: bool, // field ini digunakan untuk menyetel apakah user bisa menggunakan karakter tersebut atau tidak } \\ Cut other code fn new() -> Self { Self { name: "Billy".to_string(), age: 15, height: 170, weight: 70, lifestate: LifeState::Alive, can_use: true, // .new() selalu mengembalikan Character, jadi secara default valuenya kita set ke true } } }
Dan untuk method lainnya seperti .height()
, kita akan setel can_use
menjadi false
. Hanya method .build()
yang akan mengubah can_use
kembali menjadi true
, so now the user has to do a final check with .build()
. We will make sure that height
is not above 200 and weight
is not above 300. Also, in our game there is a bad word called smurf
that we don't want characters to use.
Beginilah method .build()
yang kita buat:
#![allow(unused)] fn main() { fn build(mut self) -> Result<Character, String> { // 🚧 if self.height < 200 && self.weight < 300 && !self.name.to_lowercase().contains("smurf") { self.can_use = true; Ok(self) } else { Err("Could not create character. Characters must have: 1) Height below 200 2) Weight below 300 3) A name that is not Smurf (that is a bad word)" .to_string()) } } }
!self.name.to_lowercase().contains("smurf")
memastikan user tidak menuliskan sesuatu seperti "SMURF" atau "IamSmurf" . Ia membuat seluruh String
tersebut menjadi lowercase (huruf kecil), dan memeriksa isinya menggunakan method .contains()
(alih-alih menggunakan ==
). Dan !
pada bagian awal tersebut adalah "not".
Jika semua inputannya sudah benar, maka kita set can_use
menjadi true
, dan berikan Character
ke user dengan dibungkus di dalam Ok
.
Sekarang code kita telah selesai. Kita akan membuat tiga karakter yang tidak bisa dibuat, dan satu karakter yang bisa dibuat. Maka, codenya sekarang menjadi seperti ini:
#[derive(Debug)] struct Character { name: String, age: u8, height: u32, weight: u32, lifestate: LifeState, can_use: bool, // Ini adalah value yang baru } #[derive(Debug)] enum LifeState { Alive, Dead, NeverAlive, Uncertain, } impl Character { fn new() -> Self { Self { name: "Billy".to_string(), age: 15, height: 170, weight: 70, lifestate: LifeState::Alive, can_use: true, // .new() secara otomatis akan menciptakan character, sehingga kita set dengan true } } fn height(mut self, height: u32) -> Self { self.height = height; self.can_use = false; // Karena data default diubah melalui method .height(), user tidak bisa menggunakan karakter tersebut self } fn weight(mut self, weight: u32) -> Self { self.weight = weight; self.can_use = false; self } fn name(mut self, name: &str) -> Self { self.name = name.to_string(); self.can_use = false; self } fn build(mut self) -> Result<Character, String> { if self.height < 200 && self.weight < 300 && !self.name.to_lowercase().contains("smurf") { self.can_use = true; // Jika semua inputan sudah sesuai, maka akan diubah kembali menjadi true Ok(self) // dan mengembalikan Character } else { Err("Could not create character. Characters must have: 1) Height below 200 2) Weight below 300 3) A name that is not Smurf (that is a bad word)" .to_string()) } } } fn main() { let character_with_smurf = Character::new().name("Lol I am Smurf!!").build(); // Berisi kata "smurf" - not okay let character_too_tall = Character::new().height(400).build(); // Terlalu tinggi - not okay let character_too_heavy = Character::new().weight(500).build(); // Terlalu berat - not okay let okay_character = Character::new() .name("Billybrobby") .height(180) .weight(100) .build(); // Karakter yang ini bisa dibuat. Namanya bisa diterima, tinggi dan beratnya juga sesuai // Kembaliannya bukan Character, melainkan Result<Character, String>. Jadi kita masukkan karakter-karakter di atas ke dalam Vec sehingga kita bisa melihatnya: let character_vec = vec![character_with_smurf, character_too_tall, character_too_heavy, okay_character]; for character in character_vec { // Sekarang kita akan mencetak karakternya jika Ok, dan mencetak error jika ia adalah Err match character { Ok(character_info) => println!("{:?}", character_info), Err(err_info) => println!("{}", err_info), } println!(); // Tambahkan jeda 1 baris } }
Hasilnya adalah:
Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)
Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)
Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)
Character { name: "Billybrobby", age: 15, height: 180, weight: 100, lifestate: Alive, can_use: true }