Skip to content

Is the Builder pattern an anti-pattern in Rust due to the Default trait? #64

@fschutt

Description

@fschutt

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,
}
  1. 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();
  1. 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.

  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    M-move-to-discussionsMeta: Label for converting issues to discussions

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions