Factory Pattern

Object creation with traits and generics

intermediate
factorycreationaltraits
🎮 Interactive Playground

What is the Factory Pattern?

The Factory pattern encapsulates object creation logic, allowing you to create objects without specifying their concrete types. In Rust, we adapt this pattern using traits and associated functions rather than traditional OOP inheritance.

The Problem

When to use factories in Rust:

  • Complex construction: Objects require multiple steps to build
  • Runtime type selection: Choosing concrete type based on config/input
  • Testability: Injecting mock implementations
  • API stability: Hiding internal types from public API

Example Code

use std::fmt::Debug;

/// Trait defining the interface for all shapes
pub trait Shape: Debug {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
    fn name(&self) -> &str;
}

/// Concrete implementation: Circle
#[derive(Debug)]
pub struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    fn perimeter(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.radius
    }

    fn name(&self) -> &str {
        "Circle"
    }
}

/// Concrete implementation: Rectangle
#[derive(Debug)]
pub struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    fn name(&self) -> &str {
        "Rectangle"
    }
}

/// Concrete implementation: Triangle
#[derive(Debug)]
pub struct Triangle {
    a: f64,
    b: f64,
    c: f64,
}

impl Shape for Triangle {
    fn area(&self) -> f64 {
        // Heron's formula
        let s = (self.a + self.b + self.c) / 2.0;
        (s * (s - self.a) * (s - self.b) * (s - self.c)).sqrt()
    }

    fn perimeter(&self) -> f64 {
        self.a + self.b + self.c
    }

    fn name(&self) -> &str {
        "Triangle"
    }
}

/// Simple factory function
pub fn create_shape(shape_type: &str, params: &[f64]) -> Option<Box<dyn Shape>> {
    match shape_type {
        "circle" => {
            if params.len() >= 1 {
                Some(Box::new(Circle { radius: params[0] }))
            } else {
                None
            }
        }
        "rectangle" => {
            if params.len() >= 2 {
                Some(Box::new(Rectangle {
                    width: params[0],
                    height: params[1],
                }))
            } else {
                None
            }
        }
        "triangle" => {
            if params.len() >= 3 {
                Some(Box::new(Triangle {
                    a: params[0],
                    b: params[1],
                    c: params[2],
                }))
            } else {
                None
            }
        }
        _ => None,
    }
}

/// Abstract Factory using trait objects
pub trait ShapeFactory {
    fn create(&self) -> Box<dyn Shape>;
}

/// Factory for creating circles of a specific size
pub struct CircleFactory {
    pub default_radius: f64,
}

impl ShapeFactory for CircleFactory {
    fn create(&self) -> Box<dyn Shape> {
        Box::new(Circle { radius: self.default_radius })
    }
}

/// Factory for creating squares (special rectangles)
pub struct SquareFactory {
    pub side: f64,
}

impl ShapeFactory for SquareFactory {
    fn create(&self) -> Box<dyn Shape> {
        Box::new(Rectangle {
            width: self.side,
            height: self.side,
        })
    }
}

/// Generic factory using closures
pub struct FunctionFactory<F>
where
    F: Fn() -> Box<dyn Shape>,
{
    factory_fn: F,
}

impl<F> FunctionFactory<F>
where
    F: Fn() -> Box<dyn Shape>,
{
    pub fn new(factory_fn: F) -> Self {
        FunctionFactory { factory_fn }
    }

    pub fn create(&self) -> Box<dyn Shape> {
        (self.factory_fn)()
    }
}

/// Factory with configuration
#[derive(Debug, Clone)]
pub struct ShapeConfig {
    pub scale: f64,
    pub default_color: String,
}

impl Default for ShapeConfig {
    fn default() -> Self {
        ShapeConfig {
            scale: 1.0,
            default_color: "black".to_string(),
        }
    }
}

pub struct ConfigurableShapeFactory {
    config: ShapeConfig,
}

impl ConfigurableShapeFactory {
    pub fn new(config: ShapeConfig) -> Self {
        ConfigurableShapeFactory { config }
    }

    pub fn create_circle(&self, radius: f64) -> Circle {
        Circle {
            radius: radius * self.config.scale,
        }
    }

    pub fn create_rectangle(&self, width: f64, height: f64) -> Rectangle {
        Rectangle {
            width: width * self.config.scale,
            height: height * self.config.scale,
        }
    }
}

/// Static factory methods (Rust idiom: associated functions)
impl Circle {
    pub fn new(radius: f64) -> Self {
        Circle { radius }
    }

    pub fn unit() -> Self {
        Circle { radius: 1.0 }
    }
}

impl Rectangle {
    pub fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }
    }

    pub fn square(side: f64) -> Self {
        Rectangle { width: side, height: side }
    }
}

fn main() {
    // Simple factory function
    let circle = create_shape("circle", &[5.0]).unwrap();
    let rect = create_shape("rectangle", &[4.0, 3.0]).unwrap();
    println!("{}: area = {:.2}", circle.name(), circle.area());
    println!("{}: area = {:.2}", rect.name(), rect.area());

    // Abstract factory
    let factories: Vec<Box<dyn ShapeFactory>> = vec![
        Box::new(CircleFactory { default_radius: 10.0 }),
        Box::new(SquareFactory { side: 5.0 }),
    ];

    for factory in &factories {
        let shape = factory.create();
        println!("Created {}: area = {:.2}", shape.name(), shape.area());
    }

    // Closure-based factory
    let custom_factory = FunctionFactory::new(|| {
        Box::new(Triangle { a: 3.0, b: 4.0, c: 5.0 })
    });
    let triangle = custom_factory.create();
    println!("{}: area = {:.2}", triangle.name(), triangle.area());

    // Configurable factory
    let config = ShapeConfig { scale: 2.0, default_color: "blue".to_string() };
    let factory = ConfigurableShapeFactory::new(config);
    let scaled_circle = factory.create_circle(5.0);
    println!("Scaled circle radius: {}", scaled_circle.radius); // 10.0

    // Static factory methods
    let unit_circle = Circle::unit();
    let square = Rectangle::square(4.0);
    println!("Unit circle area: {:.2}", unit_circle.area());
    println!("Square area: {:.2}", square.area());
}

Why This Works

  1. Trait objects (Box): Enable runtime polymorphism
  2. Associated functions: Rust's idiomatic "constructor" pattern
  3. Closure factories: Flexible, capture environment
  4. Configuration structs: Carry factory state without mutation

Factory Variants in Rust

| Pattern | Use Case | Rust Implementation |

|---------|----------|---------------------|

| Simple Factory | String → concrete type | Function returning Box |

| Factory Method | Subclass decides type | Trait with create() method |

| Abstract Factory | Family of related objects | Trait returning related trait objects |

| Static Factory | Named constructors | Associated functions (impl Type) |

When to Use

  • Plugin systems: Load implementations at runtime
  • Configuration-driven creation: Create objects based on config files
  • Testing: Inject mock implementations
  • API design: Hide implementation details

⚠️ Anti-patterns

// DON'T: Factory that just calls new()
fn useless_factory() -> MyType {
    MyType::new() // Just call new() directly
}

// DON'T: Overly complex factory for simple types
struct OverEngineeredFactory;
impl OverEngineeredFactory {
    fn create_string(&self) -> String {
        String::new() // Just use String::new()
    }
}

// DO: Use factory when there's real logic
fn create_connection(config: &Config) -> Result<Connection, Error> {
    // Validation, retry logic, etc.
}

Real-World Usage

  • serde: Deserialize trait creates types from various formats
  • tokio: Runtime builders create configured runtimes
  • reqwest: Client::builder() for HTTP client configuration
  • Database drivers: Connection pool factories

Exercises

  1. Add a ShapeRegistry that maps string names to factory functions
  2. Implement an async factory that fetches configuration from a server
  3. Create a factory that validates inputs and returns Result
  4. Build a factory with caching (returns same instance for same params)

🎮 Try it Yourself

🎮

Factory Pattern - Playground

Run this code in the official Rust Playground