Design: Reusable builder and cloning vs. references

I'm developing a builder struct (here called Builder) that builds an instance (here called Product) of a foreign trait for use with another library, and I can't decide how best to handle ownership of the fields involved.

Aside from plain build(), the builder provides several convenience methods that call build() and pass the Product through the various hoops of typical usage, consuming the Product at the end, and it is very likely that a user will want to call these convenience methods multiple times. Hence, the build() method needs to take &self (unless the convenience methods clone the builder and call build() on the clone, which sounds ridiculous).

Now, some of Builder's fields are strings (specifically, smartstrings), and while most uses of Product (including by the convenience methods) would work just fine if the type contained references to these strings and thus was bound to the Builder's lifetime, it is likely that some users will want to write dedicated functions that construct a Builder, build a Product, discard the Builder, and return the Product, in which case Product cannot be constrained by the Builder's lifetime, which would mean (since build() doesn't consume the Builder), we'd have to clone the string fields, which feels wasteful to me.

Hence, my question: Should (by the standards of best practices or ergonomics or idiomatics or whatever) Product be bound to Builder's lifetime or not? Or should Builder have a separate build method for each purpose (something I've never encountered before), i.e., build(&self) -> ProductOwned and (just spitballing a method name) build_ref(&self) -> Product<'_>?

You said instance of a foreign trait. Did you mean a trait object? I'll assume so below.

In general for a maybe-borrowing builder, I would expect something like

// n.b. not `&self`
fn build(self) -> Box<dyn Product /* + 'static */> 

// Whichever makes more sense
fn as_product(&self) -> &dyn Product 
fn as_product(&self) -> Box<dyn Product + '_>

Then have the convenience methods use as_product, not build.

1 Like

No, I mean that Product is a struct type and there's an impl thirdpartycrate::SomeTrait for Product { ... } block in my code. Is "instance of a foreign trait" not the right term for that? The fact that Product implements a foreign trait isn't really all that relevant to my question; I just included it as background.

Oh, I get it. So Product and potenially BorrowedProduct<'_> are in fact local types you control.

That doesn't change the shape of my answer really. So the question basically comes down to, is it worth the trouble to implement both the owned and borrowed versions? Given that you want to provide an owned version [1], but are concerned enough about speed that you're using smartstring and still trying to avoid cloning, the answer is probably yes.

Though it's possible you're overthinking / pre-optimizing and you don't need either.

It's just a type that implements a foreign trait. Trait's aren't classes; there's no trait-based subtyping, traits don't have constructors, a type can implement a great many traits, etc. So I don't really think of a type that implements a trait as "an instance of Trait". It exists independently of the trait.

  1. I probably wouldn't call something that didn't do that a "builder" ↩ī¸Ž