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:
pub struct Window {
pub title: &'static str,
pub width: usize,
pub height: usize,
}
- The builder pattern produces way too much code on the creators side while not having a significant amount of code reduction on the users side. This is especially visible if the struct has more fields:
Creator:
// Builder pattern
pub struct WindowBuilder {
__title: &'static str,
__width: usize,
__height: usize,
}
impl WindowBuilder {
pub fn new() -> Self {
Self {
__title: "Default title",
__width: 800,
__title: 600,
}
}
pub fn with_title(self, title: &'static str) -> Self {
Self {
__title: title,
__width: self.width,
__title: self.height,
}
}
pub fn with_dimensions(self, width: usize, height: usize) -> Self {
Self {
__title: self.title,
__width: width,
__title: height,
}
}
pub fn build(self) -> Window {
Window {
title: self.title,
width: self.width,
height: self.height,
}
}
}
// Default pattern: much less code!
impl Default for Window {
fn default() -> Self {
Self {
title: "Default title",
width: 800,
height: 600,
}
}
}
See how much code we need to construct a window in comparison to the Default
trait?
User:
// Default pattern
let window = Window {
title: "Original title",
.. Default::default()
};
// Builder pattern: not a significant reduction of usage code!
let window = WindowBuilder::new()
.with_title("Original title")
.build();
- The builder pattern doesn't protect against double-initialization:
let window = WindowBuilder::new()
.with_title("Original title")
.with_dimensions(800, 600)
.with_title("Oops, overwritten title!")
.build();
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.
- The
Default
trait eliminates the need for the SomethingBuilder
struct. The SomethingBuilder
struct is an intermediate struct that provides a certain kind of type safety so that you have to call SomethingBuilder.build()
to construct a Something
out of a SomethingBuilder
. All of this is unnecessary if you use the Default
trait - less code with essentially the same outcome. The SomethingBuilder
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 a default()
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.
move-to-discussions