Default + builder pattern

pull/40/head
Dhghomon 4 years ago committed by GitHub
parent ba977ad4d9
commit cddf839e32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,7 +11,6 @@ It is now late July, and *Easy Rust* is about 200 pages long. I am still writing
- [Rust Playground](#rust-playground)
- [Types](#types)
- [Primitive types](#primitive-types)
- [Chars](#chars)
- [Type inference](#type-inference)
- [Floats](#floats)
- [Printing 'hello, world!'](#printing-hello-world)
@ -77,7 +76,7 @@ It is now late July, and *Easy Rust* is about 200 pages long. I am still writing
- [Rc](#rc)
- [Multiple threads](#multiple-threads)
- [Closures in functions](#closures-in-functions)
- [Impl Trait](#impl-trait)
- [impl Trait](#impl-trait)
- [Arc](#arc)
- [Channels](#channels)
- [Reading Rust documentation](#reading-rust-documentation)
@ -86,6 +85,7 @@ It is now late July, and *Easy Rust* is about 200 pages long. I am still writing
- [[src] button](#src-button)
- [Information on traits](#information-on-traits)
- [Box](#box)
- [Default and the builder pattern](#default-and-the-builder-pattern)
## Rust Playground
@ -7758,3 +7758,447 @@ fn main() {
```
Even without data it is a bit complicated, and Rust does not use this type of pattern very much. This is because Rust has strict rules on borrowing and ownership, as you know. But if you want to start a list like this (a linked list), `Box` can help.
## Default and the builder pattern
You can implement a trait called `Default` that will 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`, and they are not surprising: 0, empty strings, `false`, etc.
```rust
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 you usually see but 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.
```rust
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:
```rust
#[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 use very small methods to change one value each time. Here is one such method for `Character`:
```rust
fn height(mut self, height: u32) -> Self { // (note: this will not compile) incomplete code snippet
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. 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:
```rust
fn height(mut self, height: u32) -> Self { // (note: this will not compile) incomplete code snippet
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. 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. So far our code looks like this:
```rust
#[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.
```rust
fn new() -> Self { // (note: this will not compile) incomplete code snippet
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:
```rust
#[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`.
```rust
#[derive(Debug)] // (note: this will not compile) incomplete code snippet
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:
```rust
fn build(mut self) -> Result<Character, String> { // (note: this will not compile) incomplete code snippet
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 "SMURF" or "IamSmurf" or something else. It turns the whole `String` into 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:
```rust
#[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:
```text
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 }
```

Loading…
Cancel
Save