Idiomatic way to construct object with some non-required fields


#1

Given a struct may have default values for some of its fields, is there a widely accepted pattern for declaring the constructor variants?

Here are the ones I’ve thought of:

  • Builder pattern (build() method returns Result<T, E>)

    Create another struct with defaults for the non-required fields.

    Pros:

    • Single consistent way of constructing an object
    • Validation can be tested on the build() method

    Cons:

    • More verbose object construction (especially when using Result<T, E>):

      let thing = Thing::new("name");
      
      // vs.
      
      let thing = ThingBuilder::new().name("name").build().expect("This should not fail");
      
      // or
      
      let thing = ThingBuilder::new()
          .name("name")
          .build()
          .expect("Failed to create Thing");
      
      // or return T instead of Result<T, E>
      
      let thing = ThingBuilder::new().name("name").build();
      
  • Multiple constructors with different names:

    Example:

    impl Thing {
        pub fn new(name: &'static str) -> Self {
            Thing {
                name: name,
                value: 1,
            }
        }
    
        pub fn new_with_value(name: &'static str, value: i32) -> Self {
            Thing {
                name: name,
                value: value,
            }
        }
    }
    

    Pros:

    • Less verbose
    • Avoids letting users create inconsistent objects

    Cons:

    • Becomes unwieldly when having many optional fields (e.g. new_with_x, new_with_y, new_with_x_and_y)
  • Setters for non-required fields (ew, but it’s here for completeness)

    Pros:

    • Not verbose

    Cons:

    • Unable to construct the full object in one statement
    • Must initially construct mutable object if non-required fields are to be set.
  • Single constructor with Option<X> for non-required fields

    Pros:

    • Easier for the library implementer

    Cons:

    • Ugly for usage, i.e. Thing::new("name", None);

So far I’ve been going with the builder pattern and always returning a Result<T, E>. I’m leaning towards returning T given the application should fail the way it would when using the Thing::new(...) syntax, but maybe there’s another way I’m missing before moving to that form


#2

Throughout my experience using Rust, I’ve seen the builder pattern, similar to how you’ve described it here, used very frequently. Your concern about complexity caused by using Result in particular is absolutely valid, however you may not find it to be as problematic as you might think. Often I will want to construct something wherein a Result is returned and then perform some small action on the constructed struct if doing so was successful. In such cases, I’ll simply use the and_then method on Result to perform the operation.

Another good idea for removing some of the verbosity or misdirection imposed by having a ThingBuilder separate from Thing itself is to copy the style used by hyper::client::Client and have methods on hyper::client::Client (Thing in your case) that distinguish between a generic Thing and a more specific one with some fields pre-determined.

As an example:

let pie_chart = Shape::new() // Create a generic `Shape`
    .circle()                // Create a `CircleBuilder` with radius=0, x=0, y=0
    .radius(32.0)            // Update radius to 32.0
    .unwrap();               // Finalize the builder and return a `Circle`

As you can see, I’m using unwrap() instead of build() in this example.

Since defining all of these methods could be quite a burden, some crates have recently been created, such as derive-builder that do most of the work for you.


#3

Thanks for that, it made me realize that there are actually two usage variants I’m concerned with:

  • Data objects: parameter values are taken from input
  • Compiled objects: values are known at compile time, and therefore cannot cause the Err case.

For the first case, I still want the Result<T, E>, whereas the latter case I can collapse the code like so:

let thing = ThingBuilder::new()
    .name("thing") // Mandatory parameter can be collapsed into new()
    .value(1)
    .build()
    .unwrap(); // Superfluous, build() should return the Item

// Collapsed:
let thing0 = ThingBuilder::new("name").build();
let thing1 = ThingBuilder::new("name").value(1).build();
// or
let thing1 = ThingBuilder::new("name")
    .value(1)
    .build();

I did have a look at the derive-builder; though I don’t think it supports trait-object fields too well (unless I misunderstood the code). But ya I do have a builder macro that helps generate the methods; shall update it to support both variants.


#4

Depending on what types of fields you’ve got, it may work to implement the Default trait, and then use the structure update syntax. For example, in Glium, the glium::DrawParameters struct includes twenty-two fields, but you can initialize it like this:

let params = glium::DrawParameters {
    line_width: Some(0.02),
    point_size: Some(0.02),
    .. Default::default()
};

target.draw(..., &params).unwrap();

If there are fields that are expensive to default, and which should not be Option<ExpensiveThing>, then this doesn’t work so well. You could use a builder type for that, another pattern Glium uses:

let mut builder = glium::glutin::WindowBuilder::new();
builder.opengl.vsync = true;

let display = builder.build_glium()
    .expect("failed to build glium window");

#5

What I like to do, and I think a lot of people do, is to implement the “builder” methods directly on the struct itself. This avoids having an extra “builder struct” and the build() method.

struct Thing {
    name: String,
    value: i32,
    unused: i32,
}

impl Thing {
    fn new() -> Self {
        Thing { name: "default", value: 0, unused: 0 }
    }

   fn with_name(n: String) -> Self {
       self.name = n;
       self
   }

   fn with_value(v: i32) -> Self {
       self.value = v;
       self
   }
   
   // ...
}


fn main() {
   let thing = Thing::new()
       .with_name("name")
       .with_value(1);
}

This avoids a lot of the boilerplate and is more pleasant to use. It’s less convenient to use when the methods return errors though. But the new questionmark syntax should help a lot here.