2
\$\begingroup\$

I have a couple of questions regarding how builder should be implemented idiomatically in Rust when it's necessary to call its parts depending on some flags.

Here's my take on three different designs of builders (also with usage example on rust-playground):

#[derive(Debug, Default)]
pub struct MutableBuilder {
    value: Option<i8>
}

impl MutableBuilder {
    pub fn add_value(&mut self, value: i8) -> &mut Self {
        self.value = self
            .value
            .map(|current| current + value)
            .or(Some(value));
        self
    }
    
    pub fn with_add_value(mut self, value: i8) -> Self {
        self.add_value(value);
        self
    }
    
    pub fn build(self) -> i8 {
        self.value.unwrap_or_default()
    }
}

#[derive(Debug, Default)]
pub struct ImmutableBuilder {
    value: Option<i8>
}

impl ImmutableBuilder {
    pub fn with_add_value(mut self, value: i8) -> Self {
        self.value = self
            .value
            .map(|current| current + value)
            .or(Some(value));
        self
    }
    
    pub fn build(self) -> i8 {
        self.value.unwrap_or_default()
    }
}

#[derive(Debug, Default)]
pub struct WithOptionBuilder {
    value: Option<i8>
}

impl WithOptionBuilder {
    pub fn with_add_value(mut self, value: impl Into<Option<i8>>) -> Self {
        self.value = value.into()
            .map(|value| self
                .value
                .map_or(value, |current| current + value)
            )
            .or(self.value);
        self
    }
    
    pub fn build(self) -> i8 {
        self.value.unwrap_or_default()
    }
}

It seems to me that MutableBuilder is not how it's usually encouraged to be done in Rust, it also mixes mutable and immutable styles somewhat, but this was my initial thought on how to implement this.

ImmutableBuilder seems to allow for less flexibility and requires reassigning the variable in the calling code if there are conditional calls.

WithOptionBuilder seems to be the most flexible, but it struggles to coerce types if I wanted to pass impl Into<ValueType> instead of ValueType because it already does impl Into<Option<ValueType>>. This can be solved by making the parameter Option<impl Into<ValueType>> but this makes boilerplate code appear at all unconditional call sites.

Another question is how to name the function that modifies value inside the builder rather than sets it. I came up with a scheme of with_{verb}_{value_name} but I am not sure if that's the usual way.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Must builders I ran across in Rust code use the MutableBuilder style pattern, but without functions corresponding to the with_add_value method.

MutableBuilder is the most flexible style because you can either use it like a fluent interface or mutable object. The other styles can only be used a fluent interfaces. The with methods can be helpful but mostly aren't worth the extra complexity of two different versions of the same method.

You should also ask whether you actually should be using a Builder at all. Sometimes it is the right tool, but sometimes you really should fill out the fields for a struct which, when appropriate, will be a nicer interface.

\$\endgroup\$
5
  • \$\begingroup\$ This question arose from a place where I was asked to implement a builder during a code review and it was a reasonable point since it improved the code somewhat. Still, you point out a valid choice to think of before implementing a builder, I shall keep it in mind wherever I may come across similar problem. \$\endgroup\$
    – sukhmel
    Feb 23, 2023 at 8:07
  • 3
    \$\begingroup\$ The biggest issue I encountered with mutable builders is that if your final .build() step is consuming, then you can't chain it, because the previous steps only return a &mut reference. \$\endgroup\$
    – Finomnis
    Feb 23, 2023 at 9:55
  • \$\begingroup\$ @Finomnis, good point. I think if you go with the mutable builder style, you'd best have build() take self by reference. \$\endgroup\$ Feb 23, 2023 at 16:27
  • \$\begingroup\$ @WinstonEwert That means you shouldn't use the mutable builder pattern if it absolutely must be consumed at the end? \$\endgroup\$
    – Finomnis
    Feb 23, 2023 at 19:15
  • 1
    \$\begingroup\$ @Finomnis, yeah, I think so. If the builder should be consumed at the end (e.g. it takes ownership of parameters), then it seems the mutable builder pattern isn't ideal. \$\endgroup\$ Feb 24, 2023 at 2:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.