Default and the builder pattern
You can implement the Default
trait to give values to a struct
or enum
that you think will be most common. The builder pattern works nicely with this to let users easily make changes when they want. First let's look at Default
. Actually, most general types in Rust already have Default
. They are not surprising: 0, "" (empty strings), false
, etc.
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); }
This prints '0', '', 'false'
.
So Default
is like the new
function except you don't have to enter anything. First we will make a struct
that doesn't implement Default
yet. It has a new
function which we use to make a character named Billy with some stats.
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); }
But maybe in our world we want most of the characters to be named Billy, age 15, height 170, weight 70, and alive. We can implement Default
so that we can just write Character::default()
. It looks like this:
#[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 ); }
It prints The character "Billy" is 15 years old.
Much easier!
Now comes the builder pattern. We will have many Billys, so we will keep the default. But a lot of other characters will be only a bit different. The builder pattern lets us chain very small methods to change one value each time. Here is one such method for Character
:
#![allow(unused)] fn main() { fn height(mut self, height: u32) -> Self { // 🚧 self.height = height; self } }
Make sure to notice that it takes a mut self
. We saw this once before, and it is not a mutable reference (&mut self
). It takes ownership of Self
and with mut
it will be mutable, even if it wasn't mutable before. That's because .height()
has full ownership and nobody else can touch it, so it is safe to be mutable. Then it just changes self.height
and returns Self
(which is Character
).
So let's have three of these builder methods. They are almost the same:
#![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 } }
Each one of those changes one variable and gives Self
back: this is what you see in the builder pattern. So now we can write something like this to make a character: let character_1 = Character::default().height(180).weight(60).name("Bobby");
. If you are building a library for someone else to use, this can make it easy for them. It's easy for the end user because it almost looks like natural English: "Give me a default character but with height of 180, weight of 60, and name of Bobby." So far our code looks like this:
#[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); }
One last method to add is usually called .build()
. This method is a sort of final check. When you give a user a method like .height()
you can make sure that they only put in a u32()
, but what if they enter 5000 for height? That might not be okay in the game you are making. We will use a final method called .build()
that returns a Result
. Inside it we will check if the user input is okay, and if it is, we will return an Ok(Self)
.
First though let's change the .new()
method. We don't want users to be free to create any kind of character anymore. So we'll move the values from impl Default
to .new()
. And now .new()
doesn't take any input.
#![allow(unused)] fn main() { fn new() -> Self { // 🚧 Self { name: "Billy".to_string(), age: 15, height: 170, weight: 70, lifestate: LifeState::Alive, } } }
That means we don't need impl Default
anymore, because .new()
has all the default values. So we can delete impl Default
.
Now our code looks like this:
#[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); }
This prints the same thing: Character { name: "Bobby", age: 15, height: 180, weight: 60, lifestate: Alive }
.
We are almost ready to write the method .build()
, but there is one problem: how do we make the user use it? Right now a user can write let x = Character::new().height(76767);
and get a Character
. There are many ways to do this, and maybe you can imagine your own. But we will add a can_use: bool
value to Character
.
#![allow(unused)] fn main() { #[derive(Debug)] // 🚧 struct Character { name: String, age: u8, height: u32, weight: u32, lifestate: LifeState, can_use: bool, // Set whether the user can use the character } \\ Cut other code fn new() -> Self { Self { name: "Billy".to_string(), age: 15, height: 170, weight: 70, lifestate: LifeState::Alive, can_use: true, // .new() always gives a good character, so it's true } } }
And for the other methods like .height()
, we will set can_use
to false
. Only .build()
will set it to true
again, 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.
Our .build()
method looks like this:
#![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")
makes sure that the user doesn't write something like "SMURF" or "IamSmurf" . It makes the whole String
lowercase (small letters), and checks for .contains()
instead of ==
. And the !
in front means "not".
If everything is okay, we set can_use
to true
, and give the character to the user inside Ok
.
Now that our code is done, we will create three characters that don't work, and one character that does work. The final code looks like this:
#[derive(Debug)] struct Character { name: String, age: u8, height: u32, weight: u32, lifestate: LifeState, can_use: bool, // Here is the new value } #[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() makes a fine character, so it is true } } fn height(mut self, height: u32) -> Self { self.height = height; self.can_use = false; // Now the user can't use the character 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; // Everything is okay, so set to true Ok(self) // and return the 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(); // This one contains "smurf" - not okay let character_too_tall = Character::new().height(400).build(); // Too tall - not okay let character_too_heavy = Character::new().weight(500).build(); // Too heavy - not okay let okay_character = Character::new() .name("Billybrobby") .height(180) .weight(100) .build(); // This character is okay. Name is fine, height and weight are fine // Now they are not Character, they are Result<Character, String>. So let's put them in a Vec so we can see them: let character_vec = vec![character_with_smurf, character_too_tall, character_too_heavy, okay_character]; for character in character_vec { // Now we will print the character if it's Ok, and print the error if it's Err match character { Ok(character_info) => println!("{:?}", character_info), Err(err_info) => println!("{}", err_info), } println!(); // Then add one more line } }
This will print:
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 }