Create another struct with defaults for the non-required fields.
Pros:
Single consistent way of constructing an object
Validation can be tested on the build() method
Cons:
More verbose object construction (especially when using Result<T, E>):
let thing = Thing::new("name");
// vs.
let thing = ThingBuilder::new().name("name").build().expect("This should not fail");
// or
let thing = ThingBuilder::new()
.name("name")
.build()
.expect("Failed to create Thing");
// or return T instead of Result<T, E>
let thing = ThingBuilder::new().name("name").build();
Becomes unwieldly when having many optional fields (e.g. new_with_x, new_with_y, new_with_x_and_y)
Setters for non-required fields (ew, but it's here for completeness)
Pros:
Not verbose
Cons:
Unable to construct the full object in one statement
Must initially construct mutable object if non-required fields are to be set.
Single constructor with Option<X> for non-required fields
Pros:
Easier for the library implementer
Cons:
Ugly for usage, i.e. Thing::new("name", None);
So far I've been going with the builder pattern and always returning a Result<T, E>. I'm leaning towards returning T given the application should fail the way it would when using the Thing::new(...) syntax, but maybe there's another way I'm missing before moving to that form
Throughout my experience using Rust, I've seen the builder pattern, similar to how you've described it here, used very frequently. Your concern about complexity caused by using Result in particular is absolutely valid, however you may not find it to be as problematic as you might think. Often I will want to construct something wherein a Result is returned and then perform some small action on the constructed struct if doing so was successful. In such cases, I'll simply use the and_then method on Result to perform the operation.
Another good idea for removing some of the verbosity or misdirection imposed by having a ThingBuilder separate from Thing itself is to copy the style used by hyper::client::Client and have methods on hyper::client::Client (Thing in your case) that distinguish between a generic Thing and a more specific one with some fields pre-determined.
As an example:
let pie_chart = Shape::new() // Create a generic `Shape`
.circle() // Create a `CircleBuilder` with radius=0, x=0, y=0
.radius(32.0) // Update radius to 32.0
.unwrap(); // Finalize the builder and return a `Circle`
As you can see, I'm using unwrap() instead of build() in this example.
Since defining all of these methods could be quite a burden, some crates have recently been created, such as derive-builder that do most of the work for you.
Thanks for that, it made me realize that there are actually two usage variants I'm concerned with:
Data objects: parameter values are taken from input
Compiled objects: values are known at compile time, and therefore cannot cause the Err case.
For the first case, I still want the Result<T, E>, whereas the latter case I can collapse the code like so:
let thing = ThingBuilder::new()
.name("thing") // Mandatory parameter can be collapsed into new()
.value(1)
.build()
.unwrap(); // Superfluous, build() should return the Item
// Collapsed:
let thing0 = ThingBuilder::new("name").build();
let thing1 = ThingBuilder::new("name").value(1).build();
// or
let thing1 = ThingBuilder::new("name")
.value(1)
.build();
I did have a look at the derive-builder; though I don't think it supports trait-object fields too well (unless I misunderstood the code). But ya I do have a builder macro that helps generate the methods; shall update it to support both variants.
Depending on what types of fields you've got, it may work to implement the Default trait, and then use the structure update syntax. For example, in Glium, the glium::DrawParameters struct includes twenty-two fields, but you can initialize it like this:
If there are fields that are expensive to default, and which should not be Option<ExpensiveThing>, then this doesn't work so well. You could use a builder type for that, another pattern Glium uses:
let mut builder = glium::glutin::WindowBuilder::new();
builder.opengl.vsync = true;
let display = builder.build_glium()
.expect("failed to build glium window");
What I like to do, and I think a lot of people do, is to implement the "builder" methods directly on the struct itself. This avoids having an extra "builder struct" and the build() method.
This avoids a lot of the boilerplate and is more pleasant to use. It's less convenient to use when the methods return errors though. But the new questionmark syntax should help a lot here.
Yeah but you have an encapsulation problem with that approach.
You're making the fields "name", "value" accessible.
That's the reason creating a separate builder structure makes sense, since you can decide what fields you want to make configurable during the creation of the structure, but sealed once it's created.