Keep FooBuilder private in builder design pattern

I am making my own crate bridging between a very simple Rust library and a specific complex technology. Because the crate being bridged focuses on simplicity I really want my library to be as simple, only have one entry point, and keep the public accessible signature as consistent and easy as possible.

My struct Foo originally was the only entry point to my library. But to provide more flexibility I provided a builder FooBuilder. The builder could be used by more advanced users wanting to specify their own specific config to the library, but the actual reason is to make the library easier to configure for users needing to specify some properties to use the library because their actual code slightly
deviates from the default implementation.

The user currently can access the builder using Foo::builder() as desired. But FooBuilder needs to be public for Foo::builder() to work. However, I don't want the library to make FooBuilder accessible so the user can also call FooBuilder::default() itself.

I fear that having FooBuilder accessible could confuse users not being familiar with the builder design pattern.

Is there a way to only make Foo::builder() accessible from my library but hide the FooBuilder struct?

mod my_lib {
    pub struct Foo {
        x: String,
    }

    impl Foo {
        pub fn new(x: String) -> Self {
            Self { x }
        }
        
        pub fn builder() -> FooBuilder {
            FooBuilder::default()
        }
        
        pub fn print_x(&self) {
            println!("{}", self.x);
        }
    }
    
    // Has to be default, otherwise: `error: type `FooBuilder` is private`
    // I don't want FooBuilder to be accessible directly. Only Foo::builder() should be an entrypoint to FooBuilder.
    pub struct FooBuilder {
        x: String,
    }
    
    impl FooBuilder {
        pub fn default() -> Self {
            Self {x: "default".into()}
        }
    
        pub fn build(self) -> Foo {
            Foo {x: self.x}
        }
        
        pub fn with_x(mut self, x: String) -> Self {
            self.x = x;
            self
        }
    } 
}

fn main() {
    // Only this should work:
    //let builder = my_lib::Foo::builder().with_x("lol".into());
    
    // This shouldn't work:
    let builder = my_lib::FooBuilder::default().with_x("lol".into());
    
    
    let foo = builder.build();
    foo.print_x();
}

You could have FooBuilder be a trait and have Foo::builder() return impl FooBuilder, maybe?

1 Like

Maybe you can just make FooBuilder::default private? Or more generally speaking, make every constructor of FooBuilder private. That way your users are forced to use Foo::builder to get a FooBuilder.

Another way to handily configure Foo could be a callback-based API similar to actix-web's App::configure.

4 Likes

This works.

This is an interesting suggestion. I think I am going to implement this to abstract the builder pattern even more. Thanks!

1 Like

I think you've found some good ways forward, but I'll go ahead and leave my opinion on what you shouldn't do.

Obviously, it has to be accessible in this sense...

...so I wonder what exactly you were aiming for here. Did you just mean that Foo::builder() is the only way to construct FooBuilder (easy enough)? Or did you mean something like hide it in the documentation as well?

If the latter, don't do that. It makes things much worse for the consumer. Their IDEs will still know it's a FooBuilder, documentation will show it as FooBuilder but not be clickable, it probably won't be clear that all the documentation is on Foo::builder, whatever documentation is on Foo::builder won't be structured (e.g. per method), etc.

Instead, just clearly document on the type that the only way to create it is the Foo::builder() method. Put it in a submodule if you don't want it on the front page docs.

This would hide the struct (and it's inherent methods and other non-auto trait impls), but just moves a bunch of questions to the trait I'd say.

  • They now have to import the trait to make use of the builder
    • Perhaps avoidable with Box<dyn FooBuilder>, though that's a pain with Self methods
  • If it's sealed, you're basically back at "the only way to obtain an implementing type is Foo::builder"
  • If it's not, and the only implementor is a non-public type, you're going to be mentioning Foo::builder still, and in any case downstream may wonder if there are other implementors or if they should implement it themselves
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.