Is the Builder
pattern an anti-pattern in Rust due to the Default
trait?
#237
Replies: 11 comments 1 reply
-
I'm sympathetic to this, specifically about code size, but have found default alone to fall short in certain cases. https://github.com/colin-kiegel/rust-derive-builder helps address the extra coding issue but doesn't make the code size issue go away. It just derives it for you at compile time. Cases were default alone fell short were roughly cases of exposing public apis. In particular ones where I didn't want to expose the internal API ( fields ) of my structs directly as pub fields to consumers outside my crate. Once you expose those it's hard to evolve an API without making breaking changes. Builders can often help with ergonomics. Methods which accept Into<> arguments are often more flexible and ergonomic for consumers. You lose those when you expose the ability to create a struct directly with it's internal fields. For point 2, I'm not sure that's an actual problem in practice. Immutible builders prevent that particular case but mutable builders can see benefit allowing users to initialize fields with default values that make sense per application and have it double initialized with an override for target cases. With default, you take that control out of the users hands. They can not implement a trait like default that's defined in another crate For point 3 it's actually useful to have a method like build to validate the semantic correctness of fields to ensure structs may only be created with those semantics. I think it's fair to say that default impls and builders complement each other. It's often useful to use both in some combination. The derive builder crate supports this. In specific cases you may only need one or the other. I don't think either in isolation in all cases is a silver bullet for struct creation. |
Beta Was this translation helpful? Give feedback.
-
Implementing
You could protect against this by tracking the initialization of individual fields at the type level, but then adding a new field to the builder becomes a breaking change, which defeats one of the purposes of builders. |
Beta Was this translation helpful? Give feedback.
-
I think your You need some essential transition in a builder pattern that requires changing the representation, normally a data structure's parts must be transformed simultaneously. As a rule, builder outputs are more opaque than their inputs too. This is a builder pattern:
What if As an aside, there were interesting discussions about private data handling in functional record updates. I think say
|
Beta Was this translation helpful? Give feedback.
-
I've never used it but maybe the As an aside, I think many builder patterns take some parameters struct with
This style will become more flexible once Rust adds nameless struct items, so maybe
|
Beta Was this translation helpful? Give feedback.
-
You've got an error in your code:
Should have been: pub fn build(self) -> Window {
Window {
title: self.__title,
width: self.__width,
height: self.__height,
}
} |
Beta Was this translation helpful? Give feedback.
-
Sorry to step in. I was looking at the builder pattern and got here. I have a proposal I just used in some code and I wanted to share, even though it may turn out to be a terrible idea (and feel free to say so). In a nutshell, I used the // A struct I want to have built
pub struct MyStruct {
// A private field
secret: u32,
}
// Builder for MyStruct
pub struct MyStructBuilder {
// A public field
pub open_data: u32,
}
impl From<MyStructBuilder> for MyStruct {
fn from(builder: MyStructBuilder) -> Self {
MyStruct {
secret: builder.open_data,
}
}
}
fn main() {
let builder = MyStructBuilder { open_data: 5 };
let my_struct = MyStruct::from(builder);
}
impl Default for MyStruct {
fn default() -> Self {
MyStruct::from(MyStructBuilder::default())
}
} If // A struct I want to have built
pub struct MyStruct {
// A private field
secret: u32,
}
// Builder for MyStruct
pub struct MyStructBuilder {
// A public field
pub open_data: i32,
}
impl From<MyStructBuilder> for Result<MyStruct, ()> {
fn from(builder: MyStructBuilder) -> Self {
if builder.open_data >= 0 {
Ok(MyStruct {
secret: builder.open_data as u32,
})
} else {
Err(())
}
}
} To justify my approach: the point is that, by using public fields in |
Beta Was this translation helpful? Give feedback.
-
https://github.com/rust-unofficial/patterns/blob/main/patterns/creational/builder.md |
Beta Was this translation helpful? Give feedback.
-
Perhaps the builder pattern doc should mention that this is about initializing private struct fields. |
Beta Was this translation helpful? Give feedback.
-
A major issue I see with the builder pattern is it seems to violate the DRY principal very badly. You have to repeat every single parameter out 2 or or even 3 times: builder struct field, setter method, and, if it needs to be in the resulting struct, there too. I'm not sure if it's unique to these particular crates or a broader issue, but both the macros I see getting recommended a lot for reducing code repetition, |
Beta Was this translation helpful? Give feedback.
-
I've done this a couple times:
It's becomes tricky whether you want |
Beta Was this translation helpful? Give feedback.
-
In some cases, you can't have default values, and you can use For example, https://doc.rust-lang.org/std/process/struct.Command.html implements a builder pattern. |
Beta Was this translation helpful? Give feedback.
-
I claim that the builder pattern is more or less an anti-pattern and that you should use the
Default
trait instead. Here's why:Let's say we have a struct:
Creator:
See how much code we need to construct a window in comparison to the
Default
trait?User:
The
Default
trait protects against that, because you can't initialize the same field twice. The builder pattern simply overwrites the field and you don't get any warning.Default
trait eliminates the need for theSomethingBuilder
struct. TheSomethingBuilder
struct is an intermediate struct that provides a certain kind of type safety so that you have to callSomethingBuilder.build()
to construct aSomething
out of aSomethingBuilder
. All of this is unnecessary if you use theDefault
trait - less code with essentially the same outcome. TheSomethingBuilder
has one appropriate use, in my opinion: When you need something to happen in the.build()
function and it needs to happen once (although you can implement this in adefault()
function, too). For example, you need to tell the OS to create a window. This is where it's appropriate to use a builder. However, I've seen the builder pattern to be completely overused, which is why I'm writing this.Often times when I'm having a struct with many fields that can have default values, it is easier to implement a default trait than to write ten or twenty builder functions. And that is why I claim that the builder pattern is actually an anti-pattern and that you should use the
Default
trait instead, wherever possible. It should at least be included somewhere in this repository.Beta Was this translation helpful? Give feedback.
All reactions