3
votes

I was looking at the Method syntax section of the Rust documentation and came across an example of the builder pattern. The CircleBuilder struct in the example below is an exact duplicate of the Circle struct. It seems like this redundant code violates the usual norms of programming.

I understand why the example created a new struct, because the creator did not want to implement the builder methods against the original Circle struct. That is fine, but is there a way to rewrite this example so that there is no redundancy--yet still keeping the nice builder interface in the main() function intact?

I tried to create an empty struct or a struct with just one throwaway element, but that did not work.

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

struct CircleBuilder {
    x: f64,
    y: f64,
    radius: f64,
}

impl CircleBuilder {
    fn new() -> CircleBuilder {
        CircleBuilder { x: 0.0, y: 0.0, radius: 1.0, }
    }

    fn x(&mut self, coordinate: f64) -> &mut CircleBuilder {
        self.x = coordinate;
        self
    }

    fn y(&mut self, coordinate: f64) -> &mut CircleBuilder {
        self.y = coordinate;
        self
    }

    fn radius(&mut self, radius: f64) -> &mut CircleBuilder {
        self.radius = radius;
        self
    }

    fn finalize(&self) -> Circle {
        Circle { x: self.x, y: self.y, radius: self.radius }
    }
}

fn main() {
    let c = CircleBuilder::new()
                .x(1.0)
                .y(2.0)
                .radius(2.0)
                .finalize();

    println!("area: {}", c.area());
    println!("x: {}", c.x);
    println!("y: {}", c.y);
}
2
The book's Circle example is a less than ideal choice to demonstrate the builder pattern. Realistic builders often contain entirely different fields than the type they are building. For example, OpenOptions uses a sophisticated builder to build File which ends up holding a single integer. The same with Command, where the resulting Child ends up holding three file handles and a process handles, again very different than the arguments passed to the builder. A builder that just repeats the type it's building looks like somewhat of an anti-pattern.user4815162342
Okay, that makes sense @user4815162342. I am familiar with the types of builders where the fields differ between the builder and the object being built. Thanks for the info.krishnab
@user4815162342 It's certainly not an anti-pattern. One of the major use cases of a builder is that it keeps the internal representation hidden. This means that extending the builder with additional fields can be done without breaking changes.BurntSushi5
@user4815162342 and remember the book is aimed at people who already know one programming language; it's not intended as an introduction to the builder pattern (or testing, or programming, or ...) but simply to the Rust-specific syntax and semantics.Shepmaster
@Shepmaster Agreed, but it doesn't mean the presented example cannot be more elegant or lifelike. The current example from the book made the OP think that code duplication is inherent to builder, it just seemed like bad style. On the other hand, builder examples from the stdlib, such as Command, don't have that kind of duplication at all and are great examples of the pattern. But the book is developed openly, so instead of typing here I can try to think of a better example and submit a PR. :)user4815162342

2 Answers

4
votes

Do Rust builder patterns have to use redundant struct code?

No. But sometimes they might. For example, consider if we wanted to have special logic (or even just complicated logic) around our constructor:

/// Width must always be greater than height!
struct HorizontalEllipse {
    width: f64,
    height: f64,
}

impl HorizontalEllipse {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.width / 2.0) * (self.height / 2.0)
    }
}

struct HorizontalEllipseBuilder {
    width: f64,
    height: f64,
}

impl HorizontalEllipseBuilder {
    fn new() -> HorizontalEllipseBuilder {
        HorizontalEllipseBuilder {
            width: 0.0,
            height: 0.0,
        }
    }

    fn width(&mut self, width: f64) -> &mut HorizontalEllipseBuilder {
        self.width = width;
        self
    }

    fn height(&mut self, height: f64) -> &mut HorizontalEllipseBuilder {
        self.height = height;
        self
    }

    fn finalize(&self) -> Result<HorizontalEllipse, String> {
        let HorizontalEllipseBuilder { height, width } = *self;
        if height >= width {
            Err("This is not horizontal".into())
        } else {
            Ok(HorizontalEllipse { width, height })
        }
    }
}

fn main() {
    let c = HorizontalEllipseBuilder::new()
        .width(1.0)
        .height(2.0)
        .finalize()
        .expect("not a valid ellipse");

    println!("area: {}", c.area());
    println!("width: {}", c.width);
    println!("height: {}", c.height);
}

Now a HorizontalEllipse knows that it is always true that width > height. We've moved that check from many potential places (each method) to one, the constructor. We then moved the constructor to a new type because it was complicated (not really, but truly complicated examples are usually... complicated).

Many builders I've seen also have "enhanced" types of the real object:

#[derive(Debug)]
struct Person {
    name: String,
}

#[derive(Debug, Default)]
struct PersonBuilder {
    name: Option<String>,
}

impl PersonBuilder {
    fn name(self, name: &str) -> Self {
        PersonBuilder { name: Some(name.into()), ..self }
    }

    fn build(self) -> Person {
        Person {
            name: self.name.unwrap_or_else(|| "Stefani Joanne Angelina Germanotta".into()),
        }
    }
}

fn main() {
    let person = PersonBuilder::default().build();
    println!("{:?}", person);

    let person = PersonBuilder::default().name("krishnab").build();
    println!("{:?}", person);
}

You don't see that in the book's example because it's trying to be simpler and not involve ownership concerns.

1
votes

This seems like the sort of thing a macro might be able to do. A quick search found the derive_builder and builder_macro crates which seem to implement this functionality.