This content originally appeared on DEV Community and was authored by Abinash Sahoo
Everybody knows we use structs to create structural data types.
For example, you are developing a game where your characters have properties like "name", "height", "power", "strength" and "weapon".
So to manage your character you would prefer a struct to group all the properties for a character.
Note: I am more familiar with Rust🦀, so I am using Rust to describe the concept but you can use any functional language of your choice.
struct Character {
name: String,
height: u16,
power: u16,
strength: u16,
weapon: WeaponType
}
enum WeaponType {
Bow,
Sword,
Stick
}
Here, we create two data types, one is a struct
to represent the character
and another one is an enum
to choose between weapons
. (I used an enum for weapon
type because we allow our character to have one weapon at a time.)
After creating the data types, you have to implement a function that creates new instances of the Character
type for players.
Let's implement a function that helps our players to create a new character
for them.
// User input -> name, height, weapon
// Program inferred -> power, strength
fn new(
name: &str,
height: u16,
power: u16,
strength: u16,
weapon: WeaponType
) -> Self {
Self {
name: name.to_string(),
height,
power,
strength,
weapon: WeaponType::Bow
}
}
In this function, we are taking inputs like name, height, power, strength and weapon to create a new character.
Obviously, power and strength are inferred by the program but for name, height and weapon we ask the user to input their choices.
Now you might be thinking that we are done, users can create their character of choice.
Wait a minute!
You have to provide a default option if a new player joins your game and they do not want or do not know how to customize (or any other reason) their character then you have to give them a default option to start with.
Now, we have to implement another function default
.
The default
function does not take any argument and returns a default character that will be the same for everyone.
impl Character {
fn new()...
fn default() -> Self {
Self {
name: "Sam".to_string(),
height: 165,
power: 67,
strength: 54,
weapon: WeaponType::Bow
}
}
}
Okay, What's the matter with writing just two functions?
But we are not done yet.
You have to provide functions to change the name, height and weapon of the character.
So, let's implement them too.
impl Character {
fn new() ...
}
fn default() ...
fn name(&mut self, new_name: &str) {
self.name = new_name.to_string();
}
fn height(&mut self, new_height: u16) {
self.height = new_height;
}
fn weapon(&mut self, new_weapon: WeaponType) {
self.weapon = new_weapon;
}
}
Now, we are done.
Now, a player can start with a custom character, also if they want to they can start with the default character and change the name, height and weapon afterwards.
But there is a problem, we are repeating ourselves.
We are writing the same type functions twice i.e. new()
and default()
.
So, to solve this, we use a builder pattern.
let's see what a builder pattern is and how it going to save us from repeating code.
Builder Pattern
Builder pattern is more like giving both default()
and new()
functionality but in one function.
Builder pattern is used in functional programming languages because we need the function chaining
feature to implement this.
Let's see how we can create a builder pattern for our above example.
First, we have to remove the new()
function.
Want to know why? Because both the new()
and default()
function does a similar thing, we can not remove the default()
as we need a default character to start with.
But we can change the name, height and weapon type of that default character.
let's see what is going to look like.
impl Character {
fn default() -> Self {
Self {
name: "Sam".to_string(),
height: 165,
power: 67,
strength: 54,
weapon: WeaponType::Bow
}
}
fn name(mut self, new_name: &str) -> Self {
self.name = new_name.to_string();
self
}
fn height(mut self, new_height: u16) -> Self {
self.height = new_height;
self
}
fn weapon(mut self, new_weapon: WeaponType) -> Self {
self.weapon = new_weapon;
self
}
}
Here we remove the new()
totally and change the update function to return a new instance of character every time we update its properties.
By doing so, we can chain the update functions with default()
so that user can customize their character at the beginning if they want to.
Here is an example code.
let john: Character = Character::default()
.name("john")
.height(175)
.weapon(WeaponType::Sword);
If we run this we will get;
Character { name: "john", height: 175, power: 67, strength: 54, weapon: Sword }
Walha! You created a custom character with ease. Also, we have the option to provide a default character if the player wants to and the player can also change the properties afterwards.
Here it is. The builder pattern provides the same functionalities with less code.
Now, let's quickly compare both.
- With
.newe()
function
// 53 LoC
struct Character {
name: String,
height: u16,
power: u16,
strength: u16,
weapon: WeaponType
}
enum WeaponType {
Bow,
Sword,
Stick
}
impl Character {
fn new(
name: &str,
height: u16,
power: u16,
strength: u16,
weapon: WeaponType
) -> Self {
Self {
name: name.to_string(),
height,
power,
strength,
weapon: WeaponType::Bow
}
}
fn default() -> Self {
Self {
name: "Sam".to_string(),
height: 165,
power: 67,
strength: 54,
weapon: WeaponType::Bow
}
}
fn name(&mut self, new_name: &str) {
self.name = new_name.to_string();
}
fn height(&mut self, new_height: u16) {
self.height = new_height;
}
fn weapon(&mut self, new_weapon: WeaponType) {
self.weapon = new_weapon;
}
}
- With builder pattern
// 40 Loc
struct Character {
name: String,
height: u16,
power: u16,
strength: u16,
weapon: WeaponType
}
enum WeaponType {
Bow,
Sword,
Stick
}
impl Character {
fn default() -> Self {
Self {
name: "Sam".to_string(),
height: 165,
power: 67,
strength: 54,
weapon: WeaponType::Bow
}
}
fn name(mut self, new_name: &str) -> Self {
self.name = new_name.to_string();
self
}
fn height(mut self, new_height: u16) -> Self {
self.height = new_height;
self
}
fn weapon(mut self, new_weapon: WeaponType) -> Self {
self.weapon = new_weapon;
self
}
}
13 lines of code, that's a lot.
Now, you know how to leverage the power of builder patterns to create new instances of structs
efficiently.
I hope you find this helpful and let me know if you are going to use it or not.
Also, I am working on an open-source guide called rust-practice
- A collection of 240+ Exercises to learn and practice building CLI tools in Rust.
Here is the link to the GitHub repo. (Do not forget to give it a ⭐)
Have a great day.
Happy Rust Journey!🦀
This content originally appeared on DEV Community and was authored by Abinash Sahoo
Abinash Sahoo | Sciencx (2024-10-12T16:20:55+00:00) Why builder pattern is more efficient than .new() while creating new struct instances?. Retrieved from https://www.scien.cx/2024/10/12/why-builder-pattern-is-more-efficient-than-new-while-creating-new-struct-instances/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.