DEV Community

Abinash Sahoo
Abinash Sahoo

Posted on

Why builder pattern is more efficient than .new() while creating new struct instances?

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
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

If we run this we will get;

Character { name: "john", height: 175, power: 67, strength: 54, weapon: Sword }
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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;
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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!🦀

Top comments (0)