Builder pattern in Rust
source link: https://www.greyblake.com/blog/2021-10-19-builder-pattern-in-rust/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Builder pattern in Rust
2021-10-19As you know, Rust does not support optional function arguments nor keyword arguments, nor function overloading. To overcome this limitation rust developers frequently apply builder pattern. It requires some extra coding, but from the API ergonomics perspective, gives a similar effect as keyword arguments and optional arguments.
Introduction to problem
Consider the following rust structure:
struct User { email: Option<String>, first_name: Option<String>, last_name: Option<String> }
In Ruby, a class that holds the same data can be defined as:
class User attr_reader :email, :first_name, :last_name def initialize(email: nil, first_name: nil, last_name: nil) @email = email @first_name = first_name @last_name = last_name end end
Don't worry much about Ruby, I just want you to show how easily a user can be created by explicitly specifying relevant fields:
greyblake = User.new( email: "[email protected]", first_name: "Sergey", )
last_name
is not there, so it gets the default value nil
automatically.
Initializing a structure in Rust
Since we do not have default arguments in Rust, in order to initialize such structure we would have to list all fields:
let greyblake = User { email: Some("[email protected]".to_string()), first_name: Some("Sergey".to_string()), last_name: None, }
This is quite similar to Ruby's keyword arguments, but we have to set all fields although last_name
is None
.
It works well, but for big complex structures, it can be verbose and annoying.
Alternatively we can implement a new()
constructor:
impl User { fn new( email: Option<String>, first_name: Option<String>, last_name: Option<String> ) -> Self { Self { email, first_name, last_name } } }
Which will be used in the following way:
let greyblake = User::new( Some("[email protected]".to_string()), Some("Sergey".to_string()), None )
But it became even worse: we still have to list values for all the fields, but now it's much easier to screw up by passing values in the wrong order (yeah, the newtype technique could help us here, but this article is not about that 🐻).
The Builder pattern to rescue
A builder is an extra structure, that provides an ergonomic interface to set values and a method to build a target structure.
Let's implement UserBuilder
that helps us to build User
:
struct UserBuilder { email: Option<String>, first_name: Option<String>, last_name: Option<String> } impl UserBuilder { fn new() -> Self { Self { email: None, first_name: None, last_name: None, } } fn email(mut self, email: impl Into<String>) -> Self { self.email = Some(email.into()); self } fn first_name(mut self, first_name: impl Into<String>) -> Self { self.first_name = Some(first_name.into()); self } fn last_name(mut self, last_name: impl Into<String>) -> Self { self.last_name = Some(last_name.into()); self } fn build(self) -> User { let Self { email, first_name, last_name } = self; User { email, first_name, last_name } } }
The things worth noticing:
- A builder resembles a target structure it builds:
UserBuilder
has the same fields asUser
has - There is one setter function per field:
email
,first_name
,last_name
- A setter function consumes a builder(
mut self
), sets a value, and returns the builder back. This enables ergonomic chain method calls. new()
creates a builder with predefined defaults (in this case all values areNone
)build()
constructs and returns the target structureUser
- it's not related to builder pattern directly, but we've updated setters to receive
impl Into<String>
instead ofString
. This makes our API more flexible.
Usually for convenience User
would implement builder()
associated function, so UserBuilder
does not have to be imported explicitly:
impl User { fn builder() -> UserBuilder { UserBuilder::new() } }
Eventually with the builder now we can construct the same user structure:
let greyblake = User::builder() .email("[email protected]") .first_name("Sergey") .build();
While it is still slightly more verbose than the Ruby version of User.new
, we got the traits we were aiming for:
- Irrelevant fields are skipped and get default values implicitly
- Relevant fields and their values are clearly spelled out
- No more noise from
Option<T>
type, no need forSome(...)
Mandatory fields
Now imagine that User
structure has mandatory fields id
and email
, this is much closer to the real life example:
struct User { id: String, email: String, first_name: Option<String>, last_name: Option<String>, }
Builder can not have reasonable defaults for id
and email
anymore, so we have to find a way to pass them.
Again, in Ruby to enforce the presence of id
and email
we would just remove default nil
values in the constructor:
class User def initialize(id:, email:, first_name: nil, last_name: nil) # ... end end
In Rust to work around the problem, we could adjust the builder's constructor to receive values of mandatory fields:
struct UserBuilder { id: String, email: String, first_name: Option<String>, last_name: Option<String>, } impl UserBuilder { fn new(id: impl Into<String>, email: impl Into<String>) -> Self { Self { id: id.into(), email: email.into(), first_name: None, last_name: None, } } fn first_name(mut self, first_name: impl Into<String>) -> Self { self.first_name = Some(first_name.into()); self } fn last_name(mut self, last_name: impl Into<String>) -> Self { self.last_name = Some(last_name.into()); self } fn build(self) -> User { let Self { id, email, first_name, last_name } = self; User { id, email, first_name, last_name } } } impl User { fn builder(id: impl Into<String>, email: impl Into<String>) -> UserBuilder { UserBuilder::new(id, email) } }
This allows us to construct a user being sure that id
and email
are always specified:
let greyblake = User::builder("13", "[email protected]") .first_name("Sergey") .build();
Unfortunately, it brings us to the same problem which we had with new()
constructor at the beginning of this article:
field names are not spelled out explicitly and it is easy to screw up by passing arguments in the wrong order.
We'll see in the next article how this can be improved with help of Phantom Builder pattern 🐻.
Links
- Discussion on Reddit
- Code on playground - code from this article on Rust Playground.
- derive_builder - Rust macro to automatically implement the builder pattern for arbitrary structs.
- Builder - Builder pattern in "Rust Design Patterns" community book.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK