Builder Pattern in Rust: A Generic Solution
In a previous postI described the Builder pattern in Rust. However, even though the solution worked, it did not look very elegant.
Hence, I found a more elegant solution, which I will present here.
Let us remind ourselves what the Builder Pattern looks like:
Let’s break this down into its parts:
- The Director. This is the client class for the Builder, and it requests some product to be built.
- The Builder interface. This is the generic interface for any Builder, and it contains the methods to build a Product.
- The ConcreteBuilder. This is the concrete class where the Product gets built. Because we can only use an interface, ConcreteBuilders can be swapped in and out to build different products.
- The product we want to build is the Product class. This could also define an interface.
Open your terminal or commandline in an empty directory and type:
cargo new rust_builder_with_generics
cd rust_builder_with_generics
We will start by defining the Bicycle struct:
#[derive(Clone,Debug)]
pub struct Bicycle {
number_of_wheels: i8,
bike_type: String
}
impl Bicycle {
fn new()->Self {
Bicycle { number_of_wheels: 0, bike_type: "".to_string() }
}
}
A bike has a number of wheels, and a bike_type. Furthermore we define a simple constructor.
The BicycleBuilder is defined as a trait:
pub trait BicycleBuilder {
fn add_wheels(&mut self)->&mut dyn BicycleBuilder;
fn set_type(&mut self)->&mut dyn BicycleBuilder;
fn build(&self) -> Bicycle;
}
In this trait we methods to add_wheels, set the type and build the finished product.
Next we will define two types of Bicycle, first the ATBBuilder:
struct ATBBuilder {
bicycle: Bicycle
}
impl BicycleBuilder for ATBBuilder {
fn add_wheels(&mut self) -> &mut dyn BicycleBuilder {
self.bicycle.number_of_wheels=2;
self
}
fn set_type(&mut self) -> &mut dyn BicycleBuilder {
self.bicycle.bike_type="ATB".to_string();
self
}
fn build(&self)->Bicycle {
self.bicycle.clone()
}
}
impl ATBBuilder {
fn new()-> Self {
Self {
bicycle: Bicycle::new()
}
}
}
This code is pretty much self-explanatory, the only addition is the constructor which in this setup is needed.
The StreetBikeBuilder is similar to the ATBBike:
struct StreetBikeBuilder {
bicycle:Bicycle
}
impl BicycleBuilder for StreetBikeBuilder {
fn add_wheels(&mut self) -> &mut dyn BicycleBuilder {
self.bicycle.number_of_wheels=3;
self
}
fn set_type(&mut self) -> &mut dyn BicycleBuilder {
self.bicycle.bike_type="Street".to_string();
self
}
fn build(&self)->Bicycle {
self.bicycle.clone()
}
}
impl StreetBikeBuilder {
fn new()->Self {
Self {
bicycle:Bicycle::new()
}
}
}
Again, very self-explanatory, also an added constructor.
Now we come to the BikeEngineer, which we will implement using generics:
struct BikeEngineer<T:BicycleBuilder> {
builder: T
}
impl<T:BicycleBuilder> BikeEngineer<T> {
fn new(builder: T)->Self {
BikeEngineer { builder: builder }
}
fn construct_bike(&mut self)->Bicycle {
self.builder
.add_wheels()
.set_type()
.build()
}
}
A short explanation:
- The BikeEngineer gets a generic parameter T with one constraint: T must implement the BicycleBuilder trait.
- The builder field therefore is of type T
- The same generic parameter and constraints are passed to the implementation.
- Since we know that builder is a type which implements the BicycleBuilder interface, the construct method has no changes.
Let’s test this:
fn main() {
let builder=StreetBikeBuilder::new();
let mut engineer=BikeEngineer::new(builder);
let bike=engineer.construct_bike();
println!("{:?}",bike);
}
Line by line:
- We first instantiate a StreetBikeBuilder
- Next we pass that to the constructor of the BikeEngineer. Notice how Rust infers the generic parameter type.
- Next we construct the bike and print it out.
As you can see, the use of generics provides us with an elegant solution to this problem, and is probably more idiomatic, that is, it is proper Rust.
Beware however, even though this works for this small and simplified problem, it might not work for every problem.