1
\$\begingroup\$

I've been looking on how to use Builder pattern on rust with structs, but all examples I've seen only use primitive data.

Given the structs:

struct Bar {
    val1: i8,
    val2: i8,
    val3: i8,
}
struct Foo {
    bar1: Bar,
    bar2: Bar,
    val1: Option<i8>,
    val2: i8,
}

The builder for Bar is quite straightforward:

#[derive(Default)]
struct BarBuilder {
    val1: Option<i8>,
    val2: Option<i8>,
    val3: Option<i8>,
}
impl BarBuilder {
    pub fn val1(&mut self, val1: i8) -> &mut Self {
        self.val1 = Some(val1);
        self
    }
    pub fn val2(&mut self, val2: i8) -> &mut Self {
        self.val2 = Some(val2);
        self
    }
    pub fn val3(&mut self, val3: i8) -> &mut Self {
        self.val3 = Some(val3);
        self
    }
    pub fn build(&mut self) -> Result<Bar, String> {
        Ok(Bar {
            val1: self.val1.ok_or("val1 empty")?,
            val2: self.val2.ok_or("val2 empty")?,
            val3: self.val3.ok_or("val3 empty")?,
        })
    }
}

But, how should I implement FooBuilder?

#[derive(Default)]
struct FooBuilder {
    //1. Which would be better Option<Bar> or BarBuilder?
    bar1: Option<Bar>,
    bar2: BarBuilder,
    val1: Option<i8>,
    val2: Option<i8>,
}
impl FooBuilder {
    //2. the data in Foo is Option<i8>, what should be the parameter? i8 or Option<i8>?
    pub fn val1(&mut self, val1: i8) -> &mut Self {
        self.val1 = Some(val1);
        self
    }
    //3. Are early validations Ok? or should just be on build?
    pub fn val2(&mut self, val2: i8) -> Result<&mut Self, String> {
        if val2 % 2 == 1 {
            Err("val2 must be pair".to_string())
        } else {
            self.val2 = Some(val2);
            Ok(self)
        }
    }
    //Expects a built Bar
    pub fn bar1(&mut self, bar1: Bar) -> &mut Self {
        self.bar1 = Some(bar1);
        self
    }
    //Returns a BarBuilder to be setted
    pub fn bar2(&mut self) -> &mut BarBuilder {
        &mut self.bar2
    }
    pub fn build(&mut self) -> Result<Foo, String> {
        Ok(Foo {
            val1: self.val1,
            val2: {
                let val2 = self.val2.ok_or("val2 empty")?;
                if val2 % 2 == 1 {
                    return Err("val2 must be pair".to_string());
                }
                val2
            },
            //We need to copy bar1, so it will have to #[derive(Clone)]
            bar1: self.bar1.clone().ok_or("bar2 empty")?,
            bar2: self.bar2.build()?
        })
    }
}

main

fn main() -> Result<(), String>{
    let expected_result = Foo {
        bar1: Bar { val1: 1, val2: 2, val3: 3 },
        bar2: Bar { val1: 3, val2: 4, val3: 5 },
        val1: Some(6),
        val2: 8,
    };
    let mut foo_builder = FooBuilder::default();
    foo_builder.val1(6)
               .bar1(BarBuilder::default().val1(1)
                                          .val2(2)
                                          .val3(3)
                                          .build()?)
               //From here on is BarBuilder, there's no way to go back
               //4. Should add .parent() and a reference to it's parent builder?
               //This becomes quite cumbersome to code in Rust with Rc, Weak and RefCell for every builder... since there is no inheritance, with macros maybe?
               .bar2().val1(3)
                      .val2(4)
                      .val3(5);
    foo_builder.val2(8)?;
    
    assert_eq!(expected_result, foo_builder.build()?);
    
    Ok(())
}

The solution I like most is having Builder' inside Builder, and adding the parent:

let mut foo_builder = FooBuilder::default::<T>();
foo_builder.val1(6)
           .bar1().val1(1)
                  .val2(2)
                  .val3(3)).parent()?
           .bar2().val1(3)
                  .val2(4)
                  .val3(5).parent()?
           .val2(8)?;

Would that be a good (but cumbersome to code) approach?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Optional or not Optional

First of all, there's no reason to abandon static checking in a builder.

In particular, this means that any mandatory field missing should lead to a compile-time error, not a runtime one.

In general, this means:

  1. Critical arguments are passed in the constructor of the builder.
  2. Non-critical mandatory arguments use a default value -- either baked in, or substituted in the build method.
  3. Setters are provided for non-critical mandatory arguments and optional arguments alike.

That is, even when designing a builder, you should aim to reduce the potential number of errors in the final build method to only those which really can't be prevented statically.

Law of Demeter

In general, you should aim to reduce the individual surface area of each component.

In particular, by forcing FooBuilder to work with a BarBuilder you preclude the user to pass a pre-built Bar. Why?

Ownership vs Borrowing

There are fluent builder styles:

  • fn setter(mut self, arg: ...) -> Self
  • fn setter(&mut self, arg: ...) -> &mut Self.

The latter allows using multiple statements or single-statement fluent-style with ease, but comes with lifetimes woes. The former is free of lifetimes woes and works well with fluent-style APIs, but requires a bit more work to use in multiple statements.

The end result is that neither is "better" than the other, and both styles are used. This may complicate getting a BarBuilder with an injected parent parameter.

Note: in fact I note this is missing from your code, only suggested in a comment.

Polymorphism

There's a missing form for the setters:

  • fn bar1<B: TryInto<Bar>>(/*&*/mut self, bar1: B) -> Result</*& mut*/Self, B::Err>

That is, you can implement impl TryFrom<BarBuilder> for Bar, and then eschew the final build()? call in:

               .bar1(BarBuilder::default().val1(1)
                                          .val2(2)
                                          .val3(3)
                                          .build()?)

Instead going with:

               .bar1(BarBuilder::default().val1(1)
                                          .val2(2)
                                          .val3(3))?

I do see it as somewhat unnecessarily complication, personally, but it is possible.

Single-Liners are not that readable

Finally, I would argue that single-liners are not that readable. Never hesitate to breaking down a complicated statements in multiple separate statements.

Ergo, my ideal API here would be:

let bar1 = BarBuilder::default().val1(1).val2(2).val3(3).build()?;
let bar2 = BarBuilder::default().val1(3).val2(4).val3(5).build()?;

let foo = FooBuilder::default().val1(6).val2(8).bar1(bar1).bar2(bar2).build()?;

assert_eq!(expected_result, foo);

Keep it Simple & Stupid (aka KISS): straightforward code, straightforward inspection of temporaries during debugging, no reference juggling or anything.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.