The Builder Pattern

Ben Lovy - Sep 6 '19 - - Dev Community

Matchmaking Like Its 2001

We're going to help John McCrea of Cake find the woman of his slightly sarcastic, oddly specific dreams. He's a particular man:

Yep, you and I both think it: this sounds like a job for the Rust compiler. This band was truly ahead of its time. Let's model the problem:

/// Girl type
struct Girl {}

impl Girl {
    /// Construct a Girl
    fn new() -> Self {
        Self {}
    }
}

/// Determine whether given girl matches spec
fn is_dream_girl(girl: &Girl) -> bool {
    // we don't know anything about spec yet, so odds are no
    false
}

fn main() {
    let girl = Girl::new();
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

Running this with cargo run yields the expected output: Match: false.

So, what is it we're looking for specifically? Luckily, our man starts right in with the preferences, on the first line he tells us he wants "a girl with a mind like a diamond". Let's add a member field to test for:

#[derive(Clone, Copy, PartialEq)]
enum Mind {
    Computer,
    Diamond,
    Garden,
    Scalpel,
    Sieve,
    Unknown,
}

struct Girl {
    mind: Mind,
}

impl Girl {
    fn new(mind: Mind) -> Self {
        Self { mind }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the test function can check for the requested variant:

fn is_dream_girl(girl: &Girl) -> bool {
    girl.mind == Mind::Diamond
}

fn main() {
    let girl = Girl::new(Mind::Diamond);
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

Great! Now we get Match: true when passing in this Girl. Hold on, though - we've got some more criteria. Next, we need "a girl who knows what's best". That's pretty easy - either she does or she doesn't:

struct Girl {
    mind: Mind,
    knows_best: bool,
}

impl Girl {
    fn new(mind: Mind, knows_best: bool) -> Self {
        Self { mind, knows_best }
    }
}

fn is_dream_girl(girl: &Girl) -> bool {
    girl.mind == Mind::Diamond && girl.knows_best
}
Enter fullscreen mode Exit fullscreen mode

Just add it to the parameter list:

fn main() {
    let girl = Girl::new(Mind::Diamond, true);
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

Groovy. Now we need "shoes that cut and eyes that burn like cigarettes". It sounds like we'll need to associate some pairs of strings:

type Attribute = (String, String);

/// Girl type
struct Girl {
    items: Vec<Attribute>,
    mind: Mind,
    knows_best: bool,
}
Enter fullscreen mode Exit fullscreen mode

An attribute will be a tuple like ("shoes", "cut"). We can ask for the shoes and eye attributes in the constructor:

impl Girl {
    fn new(mind: Mind, knows_best: bool, shoes: &str, eyes: &str) -> Self {
        let mut ret = Self {
            items: Vec::new(),
            mind,
            knows_best,
        };
        ret.push_item("shoes", shoes);
        ret.push_item("eyes", eyes);
        ret
    }

    fn push_item(&mut self, item_name: &str, attribute: &str) {
        self.items.push((item_name.into(), attribute.into()));
    }
}
Enter fullscreen mode Exit fullscreen mode

We'll just check through the items to make sure we get what we want:

fn is_dream_girl(girl: &Girl) -> bool {
    let mut found_shoes = false;
    let mut found_eyes = false;
    for item in &girl.items {
        if item.0 == "shoes" && item.1 == "cut" {
            found_shoes = true;
        } else if item.0 == "eyes" && item.1 == "burn like cigarettes" {
            found_eyes = true;
        }
    }
    girl.mind == Mind::Diamond && girl.knows_best && found_shoes && found_eyes
}
Enter fullscreen mode Exit fullscreen mode

Awesome! We just need to construct the Girl with the new attributes:

fn main() {
    let girl = Girl::new(Mind::Diamond, true, "cut", "burn like cigarettes");
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

Okay. Hold on. Do you see the problem here? Let's skim ahead...

I want a girl with the right allocations
Who's fast and thorough
And sharp as a tack
She's playing with her jewelry
She's putting up her hair
She's touring the facility
And picking up slack
...
Enter fullscreen mode Exit fullscreen mode

It just continues from there! This Girl constructor is already getting out of hand and we just barely made it out of the first stanza. What if John changes his mind? He might decide something's not as important, or add a new criterion. This code is not amenable to changes like that, every call site is dependent on this exact parameter list given in this exact order, but people don't work like that. There could be all sorts of variations.

The Pattern

Let's re-implement this program leveraging the Builder Pattern. When a Girl is first constructed, we just want to start with some sensible defaults:

struct Girl {
    items: Vec<Attribute>,
    mind: Mind,
    knows_best: bool,
}

impl Girl {
    fn new() -> Self {
        Self::default()
    }
}

impl Default for Girl {
    fn default() -> Self {
        Self { mind: Mind::Unknown, knows_best: false, items: Vec::new() }
    }
}
Enter fullscreen mode Exit fullscreen mode

Everything else is a blank slate. This way we can just use Girl::new() with no parameters and get a starting point. To add more, we can define methods:

impl Girl {
    // ..

    fn set_mind(&mut self, mind: Mind) -> &mut Self {
        self.mind = mind;
        self
    }
}
Enter fullscreen mode Exit fullscreen mode

This method takes a mutable reference and returns one, so we can construct first and adjust later:

fn main() {
    let mut girl = Girl::new();
    girl.set_mind(Mind::Diamond);
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

Let's add the rest:

impl Girl {
    fn push_item(&mut self, item_name: &str, attribute: &str) {
        self.items.push((item_name.into(), attribute.into()));
    }

    fn toggle_knows_best(&mut self) -> &mut Self {
        self.knows_best = !self.knows_best;
        self
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can add them one at a time, regardless of what we know at the time of construction:

fn main() {
    let mut girl = Girl::new();
    girl.set_mind(Mind::Diamond);
    girl.toggle_knows_best();
    girl.push_item("shoes", "cut");
    girl.push_item("eyes", "burn like cigarettes");
    println!("Match: {}", is_dream_girl(&girl));
}
Enter fullscreen mode Exit fullscreen mode

This is so much easier to work with as the specification grows and evolves.

More complex scenarios may require you to use a separate type, like GirlBuilder, and take ownership at each step. This will allow you to do this all in a one-liner: let girl = GirlBuilder::new().set_mind(Mind::Diamond).toggle_knows_best(); This does limit your configuration options, for instance if you want to conditionally call some builder method in an if expression. If possible, the non-owning pattern here is more flexible.

Here's hoping we can help Mr. McCrea finally settle down after all this time.

Challenge

Prepare yourself for five years from now when this song is an "oldie".

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .