Factory + Builder Pattern

This tutorial demonstrates the powerful combination of Factory and Builder patterns in Rust. It’s useful to know, and also be able to recognize.

Key Concepts Covered:

Factory Pattern: The DatabaseType enum acts as a factory that knows how to create different database implementations. It encapsulates the creation logic and provides a clean interface for object instantiation.

Builder Pattern: The DatabaseBuilder<T> struct provides a fluent API for configuring objects step by step. It handles optional parameters gracefully and provides sensible defaults.

The Magic Combination: The DatabaseType::builder() method is where these patterns meet – it returns a configured builder that can create the appropriate database type.

The Three-Component System:

  1. Generic Trait (Database) – The “what”
    • Defines the interface all products must implement
    • Enables polymorphic usage
    • Allows writing functions that work with any implementation
  2. Builder (DatabaseBuilder<T>) – The “how to configure”
    • Manages configuration with fluent API
    • Holds a reference to the factory
    • Provides type safety and validation
    • Delegates actual creation to the factory
  3. Factory (DatabaseFactory) – The “how to create”
    • Encapsulates creation logic
    • Knows how to instantiate different types
    • Returns objects implementing the generic trait
//! Breaking down the Factory + Builder Pattern
//! 
//! This shows the three key components and how they interact

// ============================================================================
// COMPONENT 1: Generic Trait (The Interface)
// ============================================================================

/// The generic trait defines what all implementations must do
/// This is the "product" interface in factory pattern terms
trait Database {
    fn connect(&self) -> Result<(), String>;
    fn execute(&self, query: &str) -> Result<Vec<String>, String>;
    fn get_info(&self) -> String;
}

// ============================================================================
// COMPONENT 2: Builder (The Configuration Manager)
// ============================================================================

/// The builder manages configuration and construction
/// It's generic over the factory type T
struct DatabaseBuilder<T> {
    host: Option<String>,
    port: Option<u16>,
    database: Option<String>,
    factory: T,  // This holds the factory!
}

impl<T> DatabaseBuilder<T> {
    /// Create new builder with a factory
    fn new(factory: T) -> Self {
        Self {
            host: None,
            port: None,
            database: None,
            factory,
        }
    }
    
    /// Fluent configuration methods
    fn host(mut self, host: &str) -> Self {
        self.host = Some(host.to_string());
        self
    }
    
    fn port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }
    
    fn database(mut self, database: &str) -> Self {
        self.database = Some(database.to_string());
        self
    }
}

/// This is where the magic happens - builder uses the factory to create objects
impl DatabaseBuilder<DatabaseFactory> {
    /// Build method delegates to the factory
    fn build(self) -> Result<Box<dyn Database>, String> {
        let config = DatabaseConfig {
            host: self.host.unwrap_or_else(|| "localhost".to_string()),
            port: self.port.unwrap_or(5432),
            database: self.database.unwrap_or_else(|| "default".to_string()),
        };
        
        // The builder asks the factory to create the object
        self.factory.create(config)
    }
}

// ============================================================================
// COMPONENT 3: Factory (The Creator)
// ============================================================================

#[derive(Debug, Clone)]
struct DatabaseConfig {
    host: String,
    port: u16,
    database: String,
}

/// The factory knows HOW to create different types of databases
#[derive(Debug, Clone)]
enum DatabaseFactory {
    Postgres,
    MySQL,
    SQLite,
}

impl DatabaseFactory {
    /// This is the factory method - it creates the actual objects
    fn create(&self, config: DatabaseConfig) -> Result<Box<dyn Database>, String> {
        match self {
            Self::Postgres => Ok(Box::new(PostgresDB::new(config))),
            Self::MySQL => Ok(Box::new(MySQLDB::new(config))),
            Self::SQLite => Ok(Box::new(SQLiteDB::new(config))),
        }
    }
    
    /// Convenience method that returns a builder configured with this factory
    fn builder(self) -> DatabaseBuilder<Self> {
        DatabaseBuilder::new(self)
    }
}

// ============================================================================
// Concrete Implementations (The Products)
// ============================================================================

struct PostgresDB {
    config: DatabaseConfig,
}

impl PostgresDB {
    fn new(config: DatabaseConfig) -> Self {
        Self { config }
    }
}

impl Database for PostgresDB {
    fn connect(&self) -> Result<(), String> {
        println!("๐Ÿ˜ Connecting to PostgreSQL: {}", self.config.host);
        Ok(())
    }
    
    fn execute(&self, query: &str) -> Result<Vec<String>, String> {
        Ok(vec![format!("PostgreSQL: {}", query)])
    }
    
    fn get_info(&self) -> String {
        format!("PostgreSQL at {}:{}", self.config.host, self.config.port)
    }
}

struct MySQLDB {
    config: DatabaseConfig,
}

impl MySQLDB {
    fn new(config: DatabaseConfig) -> Self {
        Self { config }
    }
}

impl Database for MySQLDB {
    fn connect(&self) -> Result<(), String> {
        println!("๐Ÿฌ Connecting to MySQL: {}", self.config.host);
        Ok(())
    }
    
    fn execute(&self, query: &str) -> Result<Vec<String>, String> {
        Ok(vec![format!("MySQL: {}", query)])
    }
    
    fn get_info(&self) -> String {
        format!("MySQL at {}:{}", self.config.host, self.config.port)
    }
}

struct SQLiteDB {
    config: DatabaseConfig,
}

impl SQLiteDB {
    fn new(config: DatabaseConfig) -> Self {
        Self { config }
    }
}

impl Database for SQLiteDB {
    fn connect(&self) -> Result<(), String> {
        println!("๐Ÿ—ƒ๏ธ Connecting to SQLite: {}", self.config.database);
        Ok(())
    }
    
    fn execute(&self, query: &str) -> Result<Vec<String>, String> {
        Ok(vec![format!("SQLite: {}", query)])
    }
    
    fn get_info(&self) -> String {
        format!("SQLite: {}", self.config.database)
    }
}

// ============================================================================
// Usage Example - See How They Work Together
// ============================================================================

fn main() -> Result<(), String> {
    println!("๐Ÿ”ง How Factory + Builder + Generic Trait Work Together\n");
    
    // Method 1: Factory creates builder, builder creates object
    println!("=== Method 1: Factory -> Builder -> Object ===");
    
    let db1 = DatabaseFactory::Postgres  // 1. Choose factory
        .builder()                        // 2. Factory creates builder  
        .host("prod-server")             // 3. Configure via builder
        .port(5432)
        .database("myapp")
        .build()?;                       // 4. Builder uses factory to create object
    
    println!("Created: {}", db1.get_info());
    
    // Method 2: Manually create builder with factory
    println!("\n=== Method 2: Manual Builder Creation ===");
    
    let db2 = DatabaseBuilder::new(DatabaseFactory::MySQL)
        .host("mysql-server")
        .database("orders")
        .build()?;
    
    println!("Created: {}", db2.get_info());
    
    // Method 3: Function that works with any database type
    println!("\n=== Method 3: Generic Function ===");
    
    create_and_test_db(DatabaseFactory::SQLite, "test.db")?;
    create_and_test_db(DatabaseFactory::Postgres, "analytics")?;
    
    Ok(())
}

/// This function shows how the generic trait lets us work with any database type
fn create_and_test_db(factory: DatabaseFactory, db_name: &str) -> Result<(), String> {
    let db = factory
        .builder()
        .database(db_name)
        .build()?;
    
    println!("Testing: {}", db.get_info());
    db.connect()?;
    let results = db.execute("SELECT * FROM users")?;
    println!("  Results: {:?}", results);
    
    Ok(())
}

This is extremely idiomatic Rust. The combination of:

  • Generics over concrete types
  • Trait objects for polymorphism
  • Fluent APIs with method chaining
  • Option<T> for optional configuration
  • Result<T, E> for error handling
  • Zero-cost abstractions

… Here’s what makes this approach so effective:.

Conclusion


So the builder is basically a configuration collector that wraps a factory. When you call build(), it doesn’t create the object itself – it passes the collected config to its internal factory and says “hey factory, make me one of these with this config.”

The factory is the actual creator, the builder is just the configuration manager that holds onto a factory.

// 1. Factory goes INTO the builder
let builder = DatabaseBuilder::new(DatabaseFactory::Postgres);

// 2. Builder holds the factory and collects config
let configured_builder = builder
    .host("localhost")
    .port(5432)
    .database("myapp");

// 3. Builder asks ITS factory to make the thing
let database = configured_builder.build()?; // calls self.factory.create(config)