Builder pattern / Using OOP and collections

Hi again,

why does the builder pattern (documentation) look like this? What is the reasoning behind everything that can be observed there?

https://doc.rust-lang.org/book/method-syntax.html#builder-pattern

I understand that I'm supposed to be free to choose the "name of the C++ new method" (constructor), so it's a factory, is this right? This is as if I had static factory methods in the scope of my C++ class, but with Rust I don't need the "ugly new and then other names" factory methods. I can choose freely right from the beginning. Makes sense. (Hopefully I did understand anything at all).

But how comes this looks like making OOP work in a non-OOP language (a bit like Lua)? There is two times the same structure.

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

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

Doesn't this violate the DRY principle?

And why actually is there a finalize method that brings it back to Circle again, and then talk about performance, move and copy semantics (somewhere, sorry). That's really a lot information and I have no formal training for anything of the design that Rust chooses, so there could really stand a bit more explanation in the book why things look like this.

One small piece of an answer,

Doesn't this violate the DRY principle?

Yes, That is a disadvantage of this style.

In python every class serves two purposes. (Sorry don't know C++ so have to use a python example.)

  1. A constructor where you can change the internals until things are set up.
  2. A object with certain invariants that are maintained.

Normally this is saved by convention.

  • Only the constructor uses it like 1, all users use it like 2. In rust this is done using fn new by convention.
  • Alter how the uses wants then call a "finalizer" that checks the invariants, then don't modify it. In rust this is done using the builder pattern.
  • The uses can modify any thing any time so every method must check the invariants just to be safe. In rust this is a struct with pub elements.

In python no matter what convention the docs say the reality is the last one. In rust if you brake the convention set by the coder of the struct the type system will not let it compile.

1 Like

But when Mozilla convert their browser rendering, which is C++, they surely will not repeat themselves? I wonder how they convert their classes, it can't really be like in this part of the book.

I do prefer structs, I virtually never declare members private, because I'm just doing this in a one-guy project, no team, no data hiding. So I'd be happy with this way. But I've got no idea if there is maybe prototype OOP possible like in Javascript, or if everything works more like C, why not. There's not much in the documentation, I'll maybe browse the web or try looking at Servo.

Is there a reason why CircleBuilder shouldn't just contain a Circle?

2 Likes

Hi there! I wrote this.

So, one thing: this is trying to keep everything as simple as possible by showing what the builder pattern looks like. I would not actually use a builder to create a Circle, if that was real code. I think most of your concerns flow from this.

In general, I agree that this example is weak for this reason.

The idea of the builder is that you can't construct an entire Circle at once; otherwise, you wouldn't use a builder.

2 Likes

Thanks for the reply. I figured out I have to look for traits, structs and enums and look what can be done with them and what not. There is no C++ to Rust, one has to carefully think about what he's doing. Got that.

Thank you again. :relaxed:

Servo, and the Rust standard library, both use the Builder pattern occasionally, but in practice (unlike that example), builders usually have a different set of members than the instance.

For example, here's OpenOptions, a builder for File from the standard library, implemented for POSIX (there's a different one for Windows):

pub struct OpenOptions {
    // generic
    read: bool,
    write: bool,
    append: bool,
    truncate: bool,
    create: bool,
    create_new: bool,
    // system-specific
    custom_flags: i32,
    mode: mode_t,
}

And here's File itself:

pub struct File(FileDesc);
pub struct FileDesc {
    fd: c_int,
}

Seems pretty DRY to me.

4 Likes

It wouldn't really matter that much if this violated DRY. Consider this function call:

let w = window::new(0, 0, 10, 11, 0, 10, 0, 9, false, true, false);

What are these arguments doing? Compare it to this:

let w = window::new()
    .visible(false)
    .position(0, 0)
    .width(10)
    .height(11)
    .exit_on_close(false)
    .blink_incessently(true)
    .border_color(0, 10, 0)
    .number_of_popups_to_spam(9)
    .build();

This has nothing to do with OOP. it's just a simple and straightforward way of implementing ad-hoc optional constructor arguments in a way that can operate as a state machine (in an easy-to-reason-about way that can validate proper construction at compile time in many cases) and giving you complete access to intermediate structures and cancellation/error detection from the outside of the constructor. Yes it's verbose. It doesn't usually violate DRY. But it's powerful, readable, and clean.

4 Likes

I think I understand it now.

I can see the builder pattern as the best way to do both, build up the data structure by copying arguments to members and run "commands" that will call operating system functions etc., like open() in the OpenOptions::new() result.

In C++ I was used to have all "data" go to the initializer lists (C++ constructors), and "commands" were in constructors and methods. It was more separated.

I mistakenly thought the Circle example were a demonstration of how to migrate from C++ classes to "Rust classes", but it's not there to demonstrate this. The duplicated structure (Circle + CircleBuilder) doesn't mean I have to write any code clones in order to translate my C++ code.

What I'm about to find out now is whether Rust can copy an argument to a struct member without the struct having the setter method (this would be like a C++ initializer), or if I'm supposed to write the setters. And I'm going to look up if open(path) really does talk to the OS and open the file. I think it does, because if not, they would have called it path(path).

Just for reference:

OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(path)
            .and_then(|mut f| f.write_all(value.as_bytes()))
            .expect("Failed")

This depends if the value is accessible or not, due to the publicity rules. That is, if it's public, anyone can, if it's not public, only stuff in the same module can.

1 Like

To be fair to a function/constructor based approach, this approach could be much more comparable if Rust had named arguments. I realize that Rust does not (and not trying to debate the merits of them for Rust in this thread) but just for comparison if Rust did have named arguments you could have a function call something like:

Example does not work in current Rust (only a theoretical example for comparision):

let w = window::new(
    position=(0, 0), 
    width=10, 
    height=11, 
    border_color=(0, 10, 0), 
    number_of_popups_to_spam=9, 
    visible=false,
    exit_on_close=true, 
    blink_incessently=false);

Coming from the Python world this is how we solve this sort of problem, so its been interesting learning about the builder pattern now working with Rust. Piston software sometimes has readability issues for me because of the lack of builder pattern/named arguments.

1 Like

Thanks for your help. I've now finally tried to create what I was looking for. It covers ~50% of my class usage in C++.

struct Animal { x: i32, y: i32 }
impl Animal {
    fn cppnew( self) -> Animal { println!("Animal c++ con"); self }
    fn look  (&self) -> String { format!("looking at you from x={}.", self.x) }
}

struct Cat { animal: Animal }
impl Cat {
	fn cppnew(self) -> Cat { println!("Cat c++ con"); self }
	fn talk(&self) -> String { "Meow".to_string() }
}

struct Dog { animal: Animal }
impl Dog {
	fn cppnew(self) -> Dog { println!("Dog c++ con"); self }
	fn talk(&self) -> String { "Ruff".to_string() }
}

fn main() {
    let cat = Cat { animal: Animal { x: 1, y: 0 }.cppnew() }.cppnew();
    let dog = Dog { animal: Animal { x: 1, y: 0 }.cppnew() }.cppnew();

    println!("cat.x is {}", cat.animal.x);
    println!("dog.y is {}", dog.animal.y);

    println!("The cat seems like {}", cat.animal.look());
    println!("The dog seems like {}", dog.animal.look());

    println!("The cat says {}!", cat.talk());
    println!("The dog says {}!", dog.talk());
}

For situations where the full builder pattern is overkill, Rust has the possibility to use different constructor functions with different args for different use cases. Given the Circle struct of the original post, A user might want to create objekts like so:

let one = Circle::new(x, y, radius);
let other = Circle::unit();
let third = Circle::at(x, y);

This would be made possible by this impl:

impl Circle {
    pub fn new(x: f64, y: f64, radius: f64) -> Self {
        Circle { x: x, y: y, radius: radius }
    }
    pub fn unit() -> Self {
        new(0.0, 0.0, 1.0)
    }
    pub fn at(x: f64, y: f64) -> Self {
        new(x, y, 1.0)
    }
}
2 Likes

Hopefully it's ok that my code with the Animal struct only shows the builder pattern in stacking those cppnew() calls. I diverted a bit to C++ again but didn't want to create a new thread.

Anyway, does anyone happen to know how I could insert Animals into an array of Animals and then let them meow or bark without knowing what Animal it is? That would be kind of the other 50% I'd need to translate my C++. I was at the Subtyping page of the book but I was a bit too unconcentrated. I'll try to read it again.

And I'd need some type safe equivalent of C++ downcasting with dynamic_ptr_cast, any form of RTTI would do.

The commonality is that both meow and bark are the primary (human English) characterization of those animal's respective vocalizations. I would tend to define a Speak trait that animals could implement that would result in a meowing action for cats and a barking action for dogs. If not all animals speak, then you could choose to only implement the trait for some animals, or have it return Option instead of just a Vocalization.

1 Like

Thanks. Now I have used traits. It's not what I really wanted, because I were not able to combine trait polymorphism and type safe trait checking, because Any (for the checking) isn't what I wanted to have as type for the array.** But it works.

** Fixed, I can cast to Any via the impl and then use ::is

use std::boxed::Box;
use std::any::Any;

struct Position {
    x: i32, y: i32
}
impl Position {
    fn build(self) -> Position { println!("running Position build code"); self }
    fn tell(&self) -> String { format!("My position is {},{}", self.x, self.y) }
}

trait Methods {
    fn tell(&self) -> String;
    fn talk(&self) -> String;
    fn any(&self) -> &Any;
}
impl Methods for Position {
    fn tell(&self) -> String { "".to_string() }
    fn talk(&self) -> String { "".to_string() }
    fn any(&self) -> &Any { self }
}

struct Cat { position: Position }
impl Cat {
    fn build(self) -> Cat { println!("running Cat build code"); self }
}
impl Methods for Cat {
    fn tell(&self) -> String { self.position.tell() }
    fn talk(&self) -> String { "Meow".to_string() }
    fn any(&self) -> &Any { self }
}

struct Dog { position: Position }
impl Dog {
    fn build(self) -> Dog { println!("running Dog build code"); self }
}
impl Methods for Dog {
    fn tell(&self) -> String { self.position.tell() }
    fn talk(&self) -> String { "Ruff".to_string() }
    fn any(&self) -> &Any { self }
}

fn main() {
    let cat = Cat { position: Position { x: 1, y: 0 }.build() }.build();
    let dog = Dog { position: Position { x: 2, y: 0 }.build() }.build();

    let mut positions: Vec<Box<Methods>> = Vec::new();

    positions.push(Box::new(cat));
    positions.push(Box::new(dog));

    println!("The positions:");

    for position in positions.iter() {
        println!("{}", position.tell());
        println!("{}", position.talk());

        if position.any().is::<Cat>() { println!("It's a cat for sure!"); };
        if position.any().is::<Dog>() { println!("It's a dog for sure!"); };
    }
}

Just recently I saw a workaround for that in the wild: You can have a constructor that takes all arguments in a struct, which implements the Default trait. You can then use ..Default::default() to fill in the ones you don't want to provide. I've seen it here (in the camera.start function):

I was looking to see how your code could be made more rustic, and got stuck trying to understand your motivation for having a concrete Animal type and needing to return it explicitly. Otherwise I would be tempted to make animal a trait. Can you elaborate on that point?

I don't get this. If Circle and CircleBuilder are in the same module and their fields are private then I see no reason why CircleBuilder shouldn't contain a Circle.

Because unless you make the Circle fields Optional, you can't construct a Circle until you have been supplied with all of its fields, eliminating any benefits of the CircleBuilder