Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
September 6, 2019 11:53 am GMT

The Builder Pattern

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 typestruct Girl {}impl Girl {    /// Construct a Girl    fn new() -> Self {        Self {}    }}/// Determine whether given girl matches specfn 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));}

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 }    }}

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));}

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}

Just add it to the parameter list:

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

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 typestruct Girl {    items: Vec<Attribute>,    mind: Mind,    knows_best: bool,}

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()));    }}

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}

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));}

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

I want a girl with the right allocationsWho's fast and thoroughAnd sharp as a tackShe's playing with her jewelryShe's putting up her hairShe's touring the facilityAnd picking up slack...

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() }    }}

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    }}

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));}

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    }}

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));}

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".


Original Link: https://dev.to/deciduously/the-builder-pattern-249l

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To