Table of Contents
Default
Usually, a good practice in the development process is the protection of the incorrect instance constructions. This protection is for the developer, when you are managing a lot of different object instances for different purposes, a clean constructor design will provide the necessary features:
The object can not permit the creation of an invalid designed object (for example, in a form, a
Person
object needs to include name and surname... Both parameters must be together).The encapsulation of the creation process... The concept of creation, it implies more steps than setting parameters. For a clean design, it implies all the necessary logic to have ready an object (domain or value object). For this reason a correct encapsulation will be provide an easy adaptation of the SOLID rule,
open for extension close for modification
It keeps the immutability in the objects, it creates new instances avoiding the internal modification (In this case, we can be sceptic because it)
It split responsibilities. Then, the code will be easier to read and maintain,
These points are only a few ones of the most important, but it can be found a lot of mores in articles from Martin Fowler or Uncle bob
In this case, Rust provides a simple feature , named Default
for avoiding a big constructor that it can be a first step in the creation of an object. Here one example:
use std::default;
use std::fmt;
struct Example {
param: String,
param2: u32,
param3: String,
optional: Option<String>,
optional2: Option<String>,
}
impl fmt::Display for Example {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"param={}, param2={}, param3={}, optional={:?}, optional2={:?}",
self.param, self.param2, self.param3, self.optional, self.optional2
)
}
}
impl Default for Example {
fn default() -> Self {
Example {
param: "default".to_string(),
param2: 1,
param3: "default3".to_string(),
optional: None,
optional2: Some("optional2".to_string()),
}
}
}
With this, you only need to use in the constructor not default params... it can simplify the creation, here we have how it can be created:
pub fn main() {
println!(
"example={:}",
Example {
param3: "anotherparam".to_string(),
..Default::default()
}
);
println!(
"example2={:}",
Example {
param2: 2,
..Default::default()
}
);
}
In both constructors, we only initialize one param (param3
and param2
), they are very useful in simple cases.
Builder pattern
Other useful design pattern is the famous builder
pattern. It provides flexibility in the creation of the instances that avoids the construction of invalid objects (instances that can not exist as an entity). Here we have an example:
#[derive(Default)]
struct ExampleBuilder {
param: String,
param2: u32,
param3: String,
optional: Option<String>,
optional2: Option<String>,
}
impl ExampleBuilder {
fn new(param: String, param2: u32, param3: String) -> Self {
ExampleBuilder {
param,
param2,
param3,
..Default::default()
}
}
pub fn optional(mut self, optional: String) -> Self {
self.optional = Some(optional);
return self;
}
pub fn optional2(mut self, optional2: String) -> Self {
self.optional2 = Some(optional2);
return self;
}
pub fn build(self) -> Example {
Example {
param: self.param,
param2: self.param2,
param3: self.param3,
optional: self.optional,
optional2: self.optional2,
}
}
It forces to include to add the mandatory parameters with the new
function... The builder pattern would create a consistent object in any step... Furthermore, if we want to include some of the optional parameters, we use optional
or optional2
functions. Ideally, in the last step, calling the build
function, it can includes the necessary post steps. Then, the creation step will be:
let example = ExampleBuilder::new("param1".to_string(), 1, "param3".to_string()).build();
//Setting up optional parameters
let example2 = ExampleBuilder::new("param1".to_string(), 1, "param3".to_string())
.optional("option1".to_string())
.optional2("option2".to_string())
.build();