There are two common ways to use the Builder pattern.
Modify by ownership:
impl Builder {
pub fn new() -> Self {
// ...
}
pub fn add_name(mut self, name: String) -> Self {
self.names.push(name);
self
}
pub fn build(self) -> Something {
// ...
}
}
Modify through reference:
impl Builder {
pub fn new() -> Self {
// ...
}
pub fn add_name(&mut self, name: String) -> &mut Self {
self.names.push(name);
self
}
pub fn build(self) -> Something {
// ...
}
}
I prefer the former, because it allows one to chain the calls from constructor, through modifiers and finally the build method to be written like this:
let something = Builder::new()
.add_name("foo".into())
.add_name("bar".into())
.other()
.stuff()
.build();
A place where it works less well is when conditionals are involved:
let bldr = Builder::new()
.add_name("foo".into());
let bldr = if some_condition {
bldr.add_name("bar".into())
} else {
bldr
};
let something = bldr
.other()
.stuff()
.build();
I went through a phase where I used the ownership version, but added a reference version of certain modifier methods, specifically meant for conditional code paths. In the end I did not like this -- it introduced a lot of repetition, but more importantly I felt it created a messy experience for the API user.
I then experimented with moving the condition into the Builder:
impl Builder {
pub fn add_name_if(mut self, cond: bool, name: String) -> Self {
if cond {
self.names.push(name);
}
self
}
}
This is more in line what I want; however it's annoying to have to specify the condition for each method call (especially if conditions are rarely used).
Next evolution was to try to introduce a somewhat generalized "conditional wrapper" method:
impl Builder {
fn cond<F>(self, b: bool, f: F) -> Self
where
F: FnOnce(Self) -> Self
{
if b {
f(self)
} else {
self
}
}
}
fn main() {
let something = Builder::new()
.add_name("foo".into())
.cond(some_bool, |this| this.add_name("bar".into()))
.build();
}
This is a little more clunky. The application needs to put up with an extra closure with a self-but-not-self argument, and it sadly introduces a per-Builder method. With that said, it has the upside of:
- not needing special conditional versions of modifier methods
- not having duplicated modifier methods for different use-cases
- allows nicely chained call chains, mixing non-conditionals and conditionals
- ergonomics favors the common case (i.e. not having a conditional (I'm pretending to know that this will always be the common case)).
.. and it does feel less clunky when more logic is added to the closure.
I finally ended up returning to where I started. I use move ownership in the modifier methods, don't add any wrapper method, and I let the application developer sort out the added complexity of handling conditionals.
However, I've recently been working on a project that happens to need conditionally called builder methods much more than I usually do, and it has made me revisit how one can improve the Builder pattern for such cases.
I'm sure this sort of thing has bothered other developers out there. Before I go on another adventure that'll eventually lead me back to square one: What does the state of the art Builder pattern look like, with regard to conditional ergonomics? Do people simply go with passing &mut self
instead?