Using the builder pattern
Sometimes you need something between the customization of the constructor and the implicitness of the default implementation. Enter the builder pattern, another technique frequently used by the Rust standard library, as it allows a caller to fluidly chain together configurations that they care about and lets them ignore details that they don't care about.
How to do it...
In the
src/bin
folder, create a file calledbuilder.rs
Add all of the following code and run it with
cargo run --bin builder
:
1 fn main() { 2 // We can easily create different configurations 3 let normal_burger = BurgerBuilder::new().build(); 4 let cheese_burger = BurgerBuilder::new() .cheese(true) .salad(false) .build(); 5 let veggie_bigmac = BurgerBuilder::new() .vegetarian(true) .patty_count(2) .build(); 6 7 if let Ok(normal_burger) = normal_burger { 8 normal_burger.print(); 9 } 10 if let Ok(cheese_burger) = cheese_burger { 11 cheese_burger.print(); 12 } 13 if let Ok(veggie_bigmac) = veggie_bigmac { 14 veggie_bigmac.print(); 15 } 16 17 // Our builder can perform a check for 18 // invalid configurations 19 let invalid_burger = BurgerBuilder::new() .vegetarian(true) .bacon(true) .build(); 20 if let Err(error) = invalid_burger { 21 println!("Failed to print burger: {}", error); 22 } 23 24 // If we omit the last step, we can reuse our builder 25 let cheese_burger_builder = BurgerBuilder::new().cheese(true); 26 for i in 1..10 { 27 let cheese_burger = cheese_burger_builder.build(); 28 if let Ok(cheese_burger) = cheese_burger { 29 println!("cheese burger number {} is ready!", i); 30 cheese_burger.print(); 31 } 32 } 33 }
This is the configurable object:
35 struct Burger { 36 patty_count: i32, 37 vegetarian: bool, 38 cheese: bool, 39 bacon: bool, 40 salad: bool, 41 } 42 impl Burger { 43 // This method is just here for illustrative purposes 44 fn print(&self) { 45 let pretty_patties = if self.patty_count == 1 { 46 "patty" 47 } else { 48 "patties" 49 }; 50 let pretty_bool = |val| if val { "" } else { "no " }; 51 let pretty_vegetarian = if self.vegetarian { "vegetarian " } else { "" }; 52 println!( 53 "This is a {}burger with {} {}, {}cheese, {}bacon and {}salad", 54 pretty_vegetarian, 55 self.patty_count, 56 pretty_patties, 57 pretty_bool(self.cheese), 58 pretty_bool(self.bacon), 59 pretty_bool(self.salad) 60 ) 61 } 62 }
And this is the builder itself. It is used to configure and create a Burger
:
64 struct BurgerBuilder { 65 patty_count: i32, 66 vegetarian: bool, 67 cheese: bool, 68 bacon: bool, 69 salad: bool, 70 } 71 impl BurgerBuilder { 72 // in the constructor, we can specify 73 // the standard values 74 fn new() -> Self { 75 BurgerBuilder { 76 patty_count: 1, 77 vegetarian: false, 78 cheese: false, 79 bacon: false, 80 salad: true, 81 } 82 } 83 84 // Now we have to define a method for every 85 // configurable value 86 fn patty_count(mut self, val: i32) -> Self { 87 self.patty_count = val; 88 self 89 } 90 91 fn vegetarian(mut self, val: bool) -> Self { 92 self.vegetarian = val; 93 self 94 } 95 fn cheese(mut self, val: bool) -> Self { 96 self.cheese = val; 97 self 98 } 99 fn bacon(mut self, val: bool) -> Self { 100 self.bacon = val; 101 self 102 } 103 fn salad(mut self, val: bool) -> Self { 104 self.salad = val; 105 self 106 } 107 108 // The final method actually constructs our object 109 fn build(&self) -> Result<Burger, String> { 110 let burger = Burger { 111 patty_count: self.patty_count, 112 vegetarian: self.vegetarian, 113 cheese: self.cheese, 114 bacon: self.bacon, 115 salad: self.salad, 116 }; 117 // Check for invalid configuration 118 if burger.vegetarian && burger.bacon { 119 Err("Sorry, but we don't server vegetarian bacon yet".to_string()) 120 } else { 121 Ok(burger) 122 } 123 } 124 }
How it works...
Whew, that's a lot of code! Let's start by breaking it up.
In the first part, we illustrate how to use this pattern to effortlessly configure a complex object. We do this by relying on sensible standard values and only specifying what we really care about:
let normal_burger = BurgerBuilder::new().build(); let cheese_burger = BurgerBuilder::new() .cheese(true) .salad(false) .build(); let veggie_bigmac = BurgerBuilder::new() .vegetarian(true) .patty_count(2) .build();
The code reads pretty nicely, doesn't it?
In our version of the builder pattern, we return the object wrapped in a Result
in order to tell the world that there are certain invalid configurations and that our builder might not always be able to produce a valid product. Because of this, we have to check the validity of our burger before accessing it[7, 10 and 13].
Our invalid configuration is vegetarian(true)
and bacon(true)
. Unfortunately, our restaurant doesn't serve vegetarian bacon yet! When you start the program, you will see that the following line will print an error:
if let Err(error) = invalid_burger { println!("Failed to print burger: {}", error); }
If we omit the final build
step, we can reuse the builder in order to build as many objects as we want. [25 to 32]
Let's see how we implemented all of this. The first thing after the main
function is the definition of our Burger
struct. No surprises here, it's just plain old data. The print
method is just here to provide us with some nice output during runtime. You can ignore it if you want.
The real logic is in the BurgerBuilder
[64]. It should have one member for every value you want to configure. As we want to configure every aspect of our burger, we will have the exact same members as Burger
. In the constructor [74], we can specify some default values. We then create one method for every configuration. In the end, in build()
[109], we first perform some error checking. If the configuration is OK, we return a Burger
made out of all of our members [121]. Otherwise, we return an error [119].
There's more...
If you want your object to be constructable without a builder, you could also provide Burger
with a Default
implementation. BurgerBuilder::new()
could then just return Default::default()
.
In build()
, if your configuration can inherently not be invalid, you can, of course, return the object directly without wrapping it in a Result
.