from thingsiplay@beehaw.org to programming@programming.dev on 26 Jul 2024 04:07
https://beehaw.org/post/15189913
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?
threaded - newest
The difference is, with a build pattern you are sure someone set the required field.
For example, actix-web you create a HttpResponse, but you don’t actually have that stuct until you finish the object by setting the body() or by using finish() to have an empty body. Before that point you have a builder.
There is noting enforcing you to set the input_directory now, before trying to use it. Depending on what you need, that is no problem. Likewise, you default the max_depth to a value before a user sets one, also fine in itself. But if the expectation is that the user should always provide their own values, then a .configre(max_depth, path) would make sense to finish of the builder.
It might not matter much here, but if what you need to set was more expensive struts, then defaulting to something might not be a good idea. Or you don’t need to have Option<PathBuf> and check every time you use it, since you know a user provided it. But that is only if it is required.
Lastly, builder make a lot of sense when there is a lot to provide, which would make creating a strict in a single function/line very complicated.
Example in non-rust: stackoverflow.com/…/when-would-you-use-the-builde…
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.
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.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.
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_
, likewith_max_depth(max_depth: usize) -> Self
.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.The basic idea behind the builder pattern is to ensure the main thing can only ever exist in a valid state, no half valid values letting you call things in weird ways that break.
Here AppSettings is not the thing you care about, the App is. So you can think of AppSettings as a builder for the App. A final call to it should construct a valid App and you should not be able to do that when the settings are invalid.
If there are one or two required fields the having those on the new method of the AppSettings or the final build method that constructs the app is a good approach. If there is a set order things need to be created in then the generic state pattern or multiple structs can be used instead to limit what functions are available at each stage.
I now understand the benefit of the two step approach. I’ll dive into this topic today and decide if its worth for my use case.
There is already a valid app (from FLTK gui) with hardcoded values so far, to get something running. Now I’m creating configuration for the program, that will include from config file, commandline options and defaults. The app is basically the gui and created as first and independent from appsettings. So in my case i would have an AppSettingsBuilder, because that is the part I am in full control over its creation.
I don’t see why you need a separate builder when you are trying to build the app, even if the app is a third part thing you can still build it as part of the final method. Assuming there are no other options needed.
(Oh no, I accidentally hit the Cancel button here… instead the Reply button. Okay, so again…)
I don’t try to build the app, that is already done in the first step automatically with
let app = app::App::default();
, which includes the FLTK logic. Then I follow with creating and configuring windows, gui, widgets, Message channels and at last call the infinite loopwhile app.wait()
, which handles the Messages and stuff.What I’m now building is appsettings, which incorporates all free standing variables and hardcoded values so far. And will include a whole another config file from a different program. And stuff I discover along the way will be added to appsettings. Idea is even to build appsettings multiple times from various sources, such as commandline options or a user configuration file. And then later mix it into the final appsettings with defaults.
So its still up in the air what the final look and functionality of the program will be. I’m still discovering while writing the tool. And I thought the builder pattern or something similar to it would fit the way I’m going with. (Sorry I tried to be shorter and more concise in my reply.)