I would like to use the builder pattern for complex arguments and being able to maintain future extensibility.
However, there seem to be various different approaches. Sometimes, the methods on the builder work on &mut self
, while others consume self
and return it again.
It's also possible to create the final struct (Hello
in case of my example below) through a method on the builder, or by using an associated function on the struct type to be created.
See the following four variants A, B, C, D:
pub struct Hello {
doit: Box<dyn Fn() -> ()>,
}
impl Hello {
pub fn doit(&self) {
(self.doit)()
}
pub fn new1(builder: &HelloBuilderMut) -> Self {
let mut suffix = String::new();
for _ in 0..builder.strength {
suffix.push('!');
}
Self {
doit: Box::new(move || println!("Hello World{suffix}")),
}
}
pub fn new2(builder: &HelloBuilderMove) -> Self {
let mut suffix = String::new();
for _ in 0..builder.strength {
suffix.push('!');
}
Self {
doit: Box::new(move || println!("Hello World{suffix}")),
}
}
}
pub struct HelloBuilderMut {
strength: u32,
}
impl HelloBuilderMut {
pub const fn new() -> Self {
Self { strength: 1 }
}
// NOTE: this method cannot be `const`
pub fn set_strength(&mut self, strength: u32) {
self.strength = strength;
}
pub fn build(&self) -> Hello {
let mut suffix = String::new();
for _ in 0..self.strength {
suffix.push('!');
}
Hello {
doit: Box::new(move || println!("Hello World{suffix}")),
}
}
}
pub struct HelloBuilderMove {
strength: u32,
}
impl HelloBuilderMove {
pub const fn new() -> Self {
Self { strength: 1 }
}
pub const fn strength(mut self, strength: u32) -> Self {
self.strength = strength;
self
}
pub fn build(&self) -> Hello {
let mut suffix = String::new();
for _ in 0..self.strength {
suffix.push('!');
}
Hello {
doit: Box::new(move || println!("Hello World{suffix}")),
}
}
}
fn main() {
// Variant A:
// - builder methods work on &mut Self
// - build method in builder to build struct
let mut builder_a = HelloBuilderMut::new();
builder_a.set_strength(1);
let hello_a = builder_a.build();
// Variant B:
// - builder methods work on &mut Self
// - associated function to build struct
let mut builder_b = HelloBuilderMut::new();
builder_b.set_strength(2);
let hello_b = Hello::new1(&builder_b);
// Variant C:
// - builder methods consume and return Self
// - build method in builder to build struct
let hello_c = HelloBuilderMove::new().strength(3).build();
// Variant D:
// - builder methods consume and return Self
// - associated function to build struct
let hello_d = Hello::new2(&HelloBuilderMove::new().strength(4));
hello_a.doit();
hello_b.doit();
hello_c.doit();
hello_d.doit();
}
Output:
Hello World!
Hello World!!
Hello World!!!
Hello World!!!!
I would like to state some pros and cons I witnessed so far:
- If the builder works on
&mut self
(variants A and B), then I can't useconst fn
. This might be a disadvantage that goes beyond syntax. Is it possible and/or planned to lift this restriction in future? - Making the builder consume
self
(variants C and D) feels like a syntax trickery. It will backlash when you conditionally set an option likeif x { builder = builder.method(…); }
where you have to repeatbuilder
(opposed toif x { builder.method(…); }
, which is more straightforward). - Having a build-method (variants A and C) is shorter. However, I came upon a case where the same "builder" struct is used to create values of several other types (maybe it's more a "configuration" struct then, instead of a "builder pattern" then). In that case, the associated methods feel more reasonable to me in that case.
Is there some consensus on what's best to do? Is the const fn
restriction a problem that might be solved in future?
Thanks in advance for your advice or ideas/thoughts/comments.