I am reading currently the Effective Java book and one of the first tips on the book is the usage of the static factory methods instead of constructors. I wanted to explore the same pattern in Rust. Traditionally, constructors are used to instantiate objects but there is a powerful alternative which many times can help in various ways, e.g. improve clarity, improving performance or provide flexibility. Let’s see how.
Let’s say that we want to create a Temperature
struct that will hold the value of the temperature in celcius, but we want to allow the users of this struct to create objects by passing either celcius or fahreneit. We could implement that in a constructor doing something like the following:
struct Temperature {
value: f64
}
#[derive(PartialEq)]
enum Unit {
Celcius,
Fahrenheit
}
impl Temperature {
fn new(value: f64, metric: Unit)-> Self {
if metric == Unit::Celcius {
Temperature {
value: value
}
} else {
Temperature {
value: (value - 32.0) * 5.0 / 9.0
}
}
}
}
The problem with this is that our constructor is bloated with an if/else statement and the users of our struct will also have to pass the unit. An alternative will be to offer explicit functions that will create the objects and act as constructors:
struct Temperature {
value: f64,
}
impl Temperature {
fn from_celsius(celsius: f64) -> Self {
Temperature { value: celsius }
}
fn from_fahrenheit(fahrenheit: f64) -> Self {
Temperature { value: (fahrenheit - 32.0) * 5.0 / 9.0 }
}
}
Unlike constructors, static factory methods can have descriptive names that make the code more readable, in addition make the interface cleaner.
Assuming we have a struct Color
, it doesn’t make sense to create multiple instances of the same color e.g. red.
struct Color {
name: String,
}
impl Color {
fn new(name: &str) -> Self {
Color {
name: name.to_string(),
}
}
}
It would have been more performant to re-use existing instances once they are requested.
struct Color {
name: &'static str,
}
impl Color {
fn get_instance(color_name: &str) -> &'static Self {
match color_name.to_lowercase().as_str() {
"red" => &Color { name: "Red" },
"green" => &Color { name: "Green" },
"blue" => &Color { name: "Blue" },
_ => &Color { name: "Unknown" },
}
}
}
The &'static str
indicates that the string slice is valid for the entire duration of the program. Any string with a static lifetime is stored in the binary of the program and will never be deallocated during the program’s execution. In that way the name
field in the Color
struct is a reference to a string that lives for the entire duration of the program. This ensures that the Color
instances can safely reference these strings without worrying about the strings going out of scope or being deallocated. The get_instance
method returns a reference to a Color
instance with a 'static
lifetime. This means that the Color instances it returns will also live for the entire duration of the program. By using static lifetime, we avoid repeatedly allocating memory for the same strings (“Red”, “Green”, “Blue”…). Instead, these strings are stored once in the program’s binary, and are not heap-allocated at runtime, which can improve performance.
In the following example, the user of our structs (the main
) has to know about the concreate Oxygen and Hydrogen structs.
trait Element {
fn symbol(&self) -> &str;
fn atomic_number(&self) -> u8;
}
struct Hydrogen;
impl Element for Hydrogen {
fn symbol(&self) -> &str {
"H"
}
fn atomic_number(&self) -> u8 {
1
}
}
struct Oxygen;
impl Element for Oxygen {
fn symbol(&self) -> &str {
"O"
}
fn atomic_number(&self) -> u8 {
8
}
}
enum ElementType {
Hydrogen,
Oxygen,
}
fn main() {
let element_type = ElementType::Hydrogen;
let element: Box<dyn Element> = match element_type {
ElementType::Hydrogen => Box::new(Hydrogen),
ElementType::Oxygen => Box::new(Oxygen),
};
println!("Element: {}, Atomic Number: {}", element.symbol(), element.atomic_number());
}
We can hide the complexity of knowing the concreate structs and those providing us flexibility to change them later by giving to the client a struct with a static method that encapsulates these details.
trait Element {
fn symbol(&self) -> &str;
fn atomic_number(&self) -> u8;
}
struct Hydrogen;
impl Element for Hydrogen {
fn symbol(&self) -> &str {
"H"
}
fn atomic_number(&self) -> u8 {
1
}
}
struct Oxygen;
impl Element for Oxygen {
fn symbol(&self) -> &str {
"O"
}
fn atomic_number(&self) -> u8 {
8
}
}
enum ElementType {
Hydrogen,
Oxygen,
}
struct ElementFactory;
impl ElementFactory {
fn create_element(element_type: ElementType) -> Box<dyn Element> {
match element_type {
ElementType::Hydrogen => Box::new(Hydrogen),
ElementType::Oxygen => Box::new(Oxygen),
}
}
}
fn main() {
let hydrogen = ElementFactory::create_element(ElementType::Hydrogen);
println!("Element: {}, Atomic Number: {}", hydrogen.symbol(), hydrogen.atomic_number());
let oxygen = ElementFactory::create_element(ElementType::Oxygen);
println!("Element: {}, Atomic Number: {}", oxygen.symbol(), oxygen.atomic_number());
}
Static factory methods and the factory function pattern are similar but have some differences. The factory function pattern is a broader concept where a function (not necessarily a static method) is used to create objects. This pattern can be used outside the context of a single class and does not require the function to be a static method of a class. The factory function can be standalone or part of a module or another class. Static factory methods are used within a class to create instances of that class or its subclasses. It’s a method with a specific naming convention to make the creation process more intuitive.