Builder Pattern in Go and Rust
The Builder Pattern is a design pattern that provides a way to construct a complex object step by step, focusing on breaking down the construction of the object into smaller, more manageable steps. It separates the construction of a object from its representation, allowing the same construction process to create different variations or representations of the final object. This helps improve code readability, especially when dealing with objects that have a large number of attributes or configurations.
Go Builder Pattern
The final code with extra bits and pieces that may not be included in the below explanation can be found HERE.
Let’s start with the Car Struct that defines the instance we would like to construct. It may contains fields (firstPrivateField and secondPrivateField) that are not known by the users of the library, maybe for library internal stuff.
// A car with specific attributes.
type Car struct {
equip Equiped
color string
carEngine Engine
firstPrivateField string
secondPrivatefield string
}
// Engine represents the type of car engine
type Engine struct {
petrol Petrol
diesel Diesel
electric Electric
}
Engine
is a struct that represents the type of a car engine. There are declared a bunch variants of the Petrol
, Diesel
, Electric
, and Equiped
as custom types to represent various aspects of a car’s engine and equipment.
CarBuilder
is a struct that acts as a builder for creating instances of the Car
struct. It contains a car
field of type Car
that will be gradually constructed.
type CarBuilder struct {
car Car
}
NewCarBuilder
is a function that initializes a new CarBuilder
instance with the specified equipment.
It sets the equip
field, as mandatory field, in the underlying Car
struct.
func NewCarBuilder(equip Equiped) *CarBuilder {
return &CarBuilder{
car: Car{
equip: equip,
},
}
}
These below methods are setter functions to different attributes of the Car struct. They take a parameter, set the corresponding field in the underlying Car
struct, and return a pointer to the CarBuilder
for method chaining.
// SetColor sets the color of the car
func (cb *CarBuilder) SetColor(color string)*CarBuilder {
cb.car.color = color
return cb
}
// SetPetrolEngine sets the petrol engine type
func (cb *CarBuilder) SetPetrolEngine(power Petrol)*CarBuilder {
cb.car.carEngine.petrol = power
return cb
}
// SetDieselEngine sets the diesel engine type
func (cb *CarBuilder) SetDieselEngine(power Diesel)*CarBuilder {
cb.car.carEngine.diesel = power
return cb
}
// SetElectricEngine sets the electric engine type
func (cb *CarBuilder) SetElectricEngine(power Electric)*CarBuilder {
cb.car.carEngine.electric = power
return cb
}
The Build
method finalizes the construction process and returns the fully constructed Car
instance.
// Build constructs and returns the final Car instance
func (cb *CarBuilder) Build() Car {
return cb.car
}
The user of the library can chain the methods to create the Car
instance:
// Example usage of Builder Pattern:
builder := car1.NewCarBuilder(car1.EquipedGold).
SetColor("Brown").
SetPetrolEngine(car1.Petrol225HP)
car_ex1 := builder.Build()
fmt.Printf("Builder Pattern: %v\n", car_ex1)
and now if we run the code:
$ go run main.go
Builder Pattern: You selected the Car equiped with Gold Package, having a Petrol 225HP engine and Brown color
Go Builder Pattern with Functional Options
The Functional Options Pattern is a flexible way to provide optional parameters to a function or constructor. In this pattern, each option is represented as a function that takes a pointer to the target struct and modifies its fields accordingly.
// Option is a functional option for configuring Car instances
type Option func(*Car)
The Car
struct is the same asabove, but instead of creating of an additional builder struct, we modify the object in place. The below functions create options for configuring different aspects of the Car. Each function returns an Option, which is essentially a closure that modifies the Car instance.
/ WithColor sets the color of the car
func WithColor(color string) Option {
return func(c *Car) {
c.color = color
}
}
// WithPetrolEngine sets the petrol engine type
func WithPetrolEngine(power Petrol) Option {
return func(c *Car) {
c.carEngine.petrol = power
}
}
// WithDieselEngine sets the diesel engine type
func WithDieselEngine(power Diesel) Option {
return func(c *Car) {
c.carEngine.diesel = power
}
}
// WithElectricEngine sets the electric engine type
func WithElectricEngine(power Electric) Option {
return func(c *Car) {
c.carEngine.electric = power
}
}
// WithEquipment sets the equipment type
func WithEquipment(equip Equiped) Option {
return func(c *Car) {
c.equip = equip
}
}
The magic happens on the code below, where the NewCar
function takes a variadic number of Option functions. It initializes a new Car
instance and applies each option to configure the Car, returning the instance of the newly created Car.
// NewCar creates a new Car instance with specified options
func NewCar(options ...Option) *Car {
car := &Car{}
for _, option := range options {
option(car)
}
return car
}
Now the user of the library can construct the Car instance using the Functional Option Pattern:
// Example usage of Functional Options Pattern
car_ex2 := car2.NewCar(
car2.WithEquipment(car2.EquipedSilver),
car2.WithColor("White"),
car2.WithElectricEngine(car2.Electric420kW),
)
fmt.Printf("Functional Options Pattern: %s\n", car_ex2)
if we run the code:
$ go run main.go
Functional Options Pattern: You selected the Car equiped with Silver Package, having a Electric 420kW engine and White color
Rust Consuming Builder Pattern
The intent is to replicate the above functionality, of constructing the Car
instance in Rust. It is well known that Rust is not a garbage collector language, and relies on ownership and borrowing as the pillars of memory safety and concurency.
The final code with extra bits and pieces that may not be included in the below explanation can be found HERE.
The Car
struct represents a car with several optional fields. We derive Default trait in order to be able to construct the struct using the fields default values. It will be usefull below while we initialize the CarBuilder
struct.
#[derive(Debug, Default)]
pub struct Car {
equip: Equiped,
color: Option<String>,
car_engine: Option<Engine>,
first_private_field: Option<String>,
second_private_field: Option<String>,
}
The Rust Enums makes easier to represent the data, the example below is the Engine enum that represents various types of engines and has three variants: Petrol
, Diesel
, and Electric
. Each variant have an associated Enum.
# [derive(Debug, Clone)]
pub enum Engine {
Petrol(Petrol),
Diesel(Diesel),
Electric(Electric),
}
Define the CarBuilder
struct, used to construct the final instance of the Car.
pub struct CarBuilder {
car: Car,
}
Below we initialize the CarBuilder
and define a set of methods that modifies the builder.
- The
new
method is a static method associated withCarBuilder
that initializes a newCarBuilder
with a specified equipmentequip
. It sets the initial state of the car field using the provided equipment and the default values for other fields. - The
set_color
&set_engine
methods takes a mutable reference to self and a value, sets the coresponding fields and returns the modified builder. - The
build
method consumes the builder (self) and constructs aResult<Car>
. It checks whether a car engine has been set, returning an error if not. It then constructs a newCar
instance using the builder’s fields, providing default values where necessary.
impl CarBuilder {
pub fn new(equip: Equiped) -> Self {
CarBuilder {
car: Car {
equip: equip,
..Default::default()
},
}
}
pub fn set_color(mut self, color: impl Into<String>) -> Self {
self.car.color = Some(color.into());
self
}
pub fn set_engine(mut self, engine: Engine) -> Self {
self.car.car_engine = Some(engine);
self
}
pub fn build(self) -> Result<Car> {
let Some(car_engine) = self.car.car_engine else {
return Err(anyhow!("You need to select an Engine type"));
};
Ok(Car {
equip: self.car.equip,
color: Some(self.car.color).unwrap_or(Some("White".to_owned())),
car_engine: Some(car_engine),
..Default::default()
})
}
}
Rust Non-Consuming Builder Pattern
Non-consuming builder pattern is usefull if we need to construct multiple instances of the Car
struct. The main difference between the two implementations lies in how ownership and mutation are handled in the builder pattern. Notice the return of &mut Self
, instead of the Self
above.
In the first example the CarBuilder
takes ownership of the Car
it is constructing and the methods like set_color
and set_engine
consume and return a modified CarBuilder
, transferring ownership. This enforces immutability within the builder after each method call.
In the below example the Car2Builder
holds ownership of the Car2
it is constructing and the methods like set_color
and set_engine
take a mutable reference (&mut self)
, allowing in-place modification of the Car2
without transferring ownership.
Notice the .clone()
while constructing the final object, it requires all types to implement the Clone trait. I derived for each type.
impl Car2Builder {
pub fn new(equip: Equiped) -> Self {
Car2Builder {
car: Car2 {
equip,
..Default::default()
},
}
}
pub fn set_color(&mut self, color: impl Into<String>) -> &mut Self {
self.car.color = Some(color.into());
self
}
pub fn set_engine(&mut self, engine: Engine) -> &mut Self {
self.car.car_engine = Some(engine);
self
}
pub fn build(&mut self) -> Result<Car2> {
let car_engine = self
.car
.car_engine
.clone()
.ok_or_else(|| anyhow!("You need to select an Engine type"))?;
Ok(Car2 {
equip: self.car.equip.clone(),
color: Some(self.car.color.clone().unwrap_or("White".to_owned())),
car_engine: Some(car_engine),
..Default::default()
})
}
}
The user of the library can utilize the builder patterns to construct the Car
instance as the examples below.
fn main() -> Result<()> {
// Create the Car using consuming Pattern
let equip = Equiped::EquipedGold;
let car = CarBuilder::new(equip)
.set_color("Blue")
.set_engine(Engine::Electric(Electric::Electric350kW))
.build()?;
println!("Consuming Builder Pattern:");
println!("You selected a {}", car);
// Create the Car using non-consuming Pattern
let equip = Equiped::EquipedPlatinum;
let mut car_builder = Car2Builder::new(equip);
let car1 = car_builder
.set_color("Black")
.set_engine(Engine::Petrol(Petrol::Petrol150HP))
.build()?;
let car2 = car_builder
.set_color("Red")
.set_engine(Engine::Diesel(Diesel::Diesel250HP))
.build()?;
println!("Non-Consuming Builder Pattern:");
println!("You selected a {}", car1);
println!("You selected a {}", car2);
Ok(())
}
and running the code
$ cargo run
Consuming Builder Pattern:
You selected a Car equiped with EquipedGold, having a Electric(Electric350kW) engine ,and Blue color.
Non-Consuming Builder Pattern:
You selected a Car equiped with EquipedPlatinum, having a Petrol(Petrol150HP) engine ,and Black color.
You selected a Car equiped with EquipedPlatinum, having a Diesel(Diesel250HP) engine ,and Red color.
Choose between the two patterns based on your preference and the specific requirements of your code. The original builder pattern is more aligned with traditional builder patterns, while the modified pattern provides a more mutable and in-place approach.
The complete code for both Go & Rust implementation is available Here.