Builder pattern in Rust
Serhii Potapov October 19, 2021 #rust #patternsAs 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:
In Ruby, a class that holds the same data can be defined as:
attr_reader :email, :first_name, :last_name
@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: ,
first_name: ,
)
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
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:
Which will be used in the following way:
let greyblake = new
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
:
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:
Eventually with the builder now we can construct the same user structure:
let greyblake = builder
.email
.first_name
.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:
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:
# ...
end
end
In Rust to work around the problem, we could adjust the builder's constructor to receive values of mandatory fields:
This allows us to construct a user being sure that id
and email
are always specified:
let greyblake = builder
.first_name
.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.