Hello folks. So I’m still not good at Rust and learn even basics after years (just on and off doing some stuff). I’m currently working on my first small GUI application with FLTK in Rust. It’s not that important for my question, but I think this gives a bit of context. The actual question is about struct and impl, using a builder pattern like pattern, but without impl builder and build() function.

Normally with the builder pattern, there are at least two structs and impl blocks. One dedicated to build the first struct. But I am doing it with only one struct and impl block, without a build() function. But it is functionally (at least conceptional) the same, isn’t it? A shorted example for illustration:

Edit: Man beehaw is ruining my code blocks removing the opening character for >, which wil be translated to < or or completely removed. I use a % to represent the opening.

struct AppSettings {
    input_directory: Option%PathBuf>,
    max_depth: u8,
}

impl AppSettings {
    fn new() -> Self {
        Self {
            input_directory: None,
            max_depth: 1,
        }
    }

    fn input_directory(mut self, path: String) -> Self {
        self.input_directory = match path.fullpath() {
            Ok(p) => Some(p),
            Err(_) => None,
        };

        self
    }

    fn max_depth(mut self, levels: u8) -> Self {
        self.max_depth = levels;

        self
    }
}

And this is then used in main like

    let mut appsettings = AppSettings::new()
        .input_directory("~/test".to_string())
        .max_depth(3);

BTW I have extended PathBuf and String with a few traits. So if you wonder why I have code like this path.fullpath() . So just ignore that part. I’m just asking about the builder pattern stuff. This works for me. Do I miss something? Why would I go and do the extra step of creating another struct and impl block to build it and a final struct, that is basically the same? I don’t get that.

Is this approach okay in your mind?

  • thingsiplay@beehaw.orgOP
    link
    fedilink
    arrow-up
    1
    ·
    2 months ago

    Enforcing setup of required fields. Ah, that makes a lot of sense! In example for my application an input_directory is required. Right now, I don’t know how to model it, so the check comes after “building” it and bail!() if its none.

    So maybe the true builder pattern is more useful for library developers, to enforce the correct way of using the functions? I need to go deeper and read more about this topic and use case. Maybe I need a different model. Or maybe it doesn’t matter in my case. Thank you for the explanation.

    • TehPers@beehaw.org
      link
      fedilink
      English
      arrow-up
      2
      ·
      2 months ago

      For library code - yes, you’d usually want to direct users to the correct way of using the library, so you’d be more likely to come across fallible build functions or a bunch of type parameters to constrain when it can be called.

      For applications - honestly, it’s your code, so do what makes sense to you. Using a build function can help you ensure your settings were fully configured before using them, but it’s up to you if that’s the direction that makes the most sense to you. One benefit is you only need to perform the check once, but the downside is having another “built” type that you need to keep in sync with the original type. You can also look at libraries like derive_builder if you want to have your builder generated for you to avoid needing to manually update two separate types.

      • thingsiplay@beehaw.orgOP
        link
        fedilink
        arrow-up
        1
        ·
        2 months ago

        Honestly, the duplication and making it more complicated for a small and (relatively) simple program is what I’m worried. On the other hand, this is a good opportunity to learn more and test it in a simple environment (no worries, its no important program). I didn’t even think about such a library, that could help with macros. I’ll give derive_builder a look today, thanks for the suggestion.

        • arendjr@programming.dev
          link
          fedilink
          arrow-up
          2
          ·
          edit-2
          2 months ago

          If you just have one or two required fields, with the rest being optional, it can also be a good middle ground to just pass the required fields to new() and use methods like in your example for the optionals.

          PS.: A common convention for these methods is to prefix them with with_, like with_max_depth(max_depth: usize) -> Self.

          • thingsiplay@beehaw.orgOP
            link
            fedilink
            arrow-up
            1
            ·
            2 months ago

            I thought about this exactly too, just to require them in new(). I’m still finding out what would be required along my research, so its not something I can plan right now. And I had named the functions with with_ prefix before, because I saw others doing it, but changed it back as this is not real builder pattern. So it may not confuse people doing this convention.