Factory Pattern - Creating Objects Without Specifying Exact Classes

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand the different types of Factory patterns (Simple Factory, Factory Method, Abstract Factory)
  • Implement Factory patterns to create objects without tight coupling to concrete classes
  • Apply Factory patterns in real-world scenarios like database drivers and UI components
  • Recognize when to use each Factory variant based on complexity and requirements
  • Refactor existing code to use Factory patterns for better maintainability and extensibility

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Think about ordering food from different restaurants through a delivery app. You don’t need to know how each restaurant prepares their food - you just specify what you want, and the right kitchen creates it. How is this similar to object creation in programming?

Lesson Overview

  • Duration: 120 minutes
  • Prerequisites: Object-oriented programming, inheritance, interfaces/abstract classes
  • Materials Needed: Development environment with Java, Python, or JavaScript support

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Factory Pattern Motivation - Problems with direct object instantiation
  2. Simple Factory - Centralizing object creation logic
  3. Factory Method Pattern - Letting subclasses decide which class to instantiate
  4. Abstract Factory Pattern - Creating families of related objects
  5. Real-world Applications - Database connections, UI components, plugin systems
  6. Design Trade-offs - When to use each pattern and complexity considerations

Success Criteria:

By the end of this session, you should be able to choose the appropriate Factory pattern for different scenarios and implement it effectively.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

What is the Factory Pattern?

The Factory pattern is a creational design pattern that provides an interface for creating objects without specifying their exact classes. Instead of calling constructors directly, client code requests objects from a factory, which decides which concrete class to instantiate.

Real-World Analogy: Consider a car manufacturing plant. You order a “sedan” or “SUV” without knowing the specific assembly process. The factory manager (Factory) decides whether to build a Honda Civic, Toyota Camry, or Ford Explorer based on your requirements.

Why This Matters:

  • Decoupling: Client code doesn’t depend on concrete classes
  • Flexibility: Easy to add new product types without changing existing code
  • Consistency: Centralized creation logic ensures proper object initialization
  • Polymorphism: Clients work with interfaces, enabling substitutability

Problems Factory Pattern Solves

Problem 1: Tight Coupling

// Bad: Client directly depends on concrete classes
public class BadOrderProcessor {
    public void processPayment(String type, double amount) {
        if (type.equals("CREDIT_CARD")) {
            CreditCardProcessor processor = new CreditCardProcessor();
            processor.process(amount);
        } else if (type.equals("PAYPAL")) {
            PayPalProcessor processor = new PayPalProcessor();
            processor.process(amount);
        } else if (type.equals("BANK_TRANSFER")) {
            BankTransferProcessor processor = new BankTransferProcessor();
            processor.process(amount);
        }
        // Adding new payment method requires modifying this code
    }
}

Problem 2: Complex Object Creation

// Bad: Complex initialization scattered throughout codebase
DatabaseConnection connection = new PostgreSQLConnection();
connection.setHost("localhost");
connection.setPort(5432);
connection.setDatabase("myapp");
connection.setUsername("user");
connection.setPassword("password");
connection.setConnectionPool(true);
connection.setMaxConnections(20);
connection.configure();
connection.connect();

Key Terminology

  • Product: Abstract interface or base class for objects being created
  • Concrete Product: Specific implementations of the product interface
  • Creator: Abstract class or interface that declares the factory method
  • Concrete Creator: Implements the factory method to create specific products
  • Factory Method: Method responsible for creating and returning product instances

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Factory Pattern Variants

1. Simple Factory (Not GoF Pattern, but Commonly Used)

The Simple Factory encapsulates object creation in a single class with a static method.

// Product interface
interface Vehicle {
    void start();
    void stop();
    String getType();
}

// Concrete products
class Car implements Vehicle {
    public void start() { System.out.println("Car engine started"); }
    public void stop() { System.out.println("Car engine stopped"); }
    public String getType() { return "Car"; }
}

class Motorcycle implements Vehicle {
    public void start() { System.out.println("Motorcycle engine started"); }
    public void stop() { System.out.println("Motorcycle engine stopped"); }
    public String getType() { return "Motorcycle"; }
}

class Truck implements Vehicle {
    public void start() { System.out.println("Truck engine started"); }
    public void stop() { System.out.println("Truck engine stopped"); }
    public String getType() { return "Truck"; }
}

// Simple Factory
class VehicleFactory {
    public static Vehicle createVehicle(String type) {
        switch (type.toUpperCase()) {
            case "CAR":
                return new Car();
            case "MOTORCYCLE":
                return new Motorcycle();
            case "TRUCK":
                return new Truck();
            default:
                throw new IllegalArgumentException("Unknown vehicle type: " + type);
        }
    }
}

// Client usage
public class VehicleRental {
    public void rentVehicle(String vehicleType) {
        Vehicle vehicle = VehicleFactory.createVehicle(vehicleType);
        vehicle.start();
        System.out.println("Renting " + vehicle.getType());
        // Use vehicle...
        vehicle.stop();
    }
}

2. Factory Method Pattern (GoF Pattern)

The Factory Method pattern defines an interface for creating objects, but lets subclasses decide which class to instantiate.

// Product interface
interface Dialog {
    void render();
    void onClick();
}

// Concrete products
class WindowsDialog implements Dialog {
    public void render() {
        System.out.println("Rendering Windows-style dialog");
    }
    
    public void onClick() {
        System.out.println("Windows dialog click handled");
    }
}

class MacDialog implements Dialog {
    public void render() {
        System.out.println("Rendering Mac-style dialog");
    }
    
    public void onClick() {
        System.out.println("Mac dialog click handled");
    }
}

class LinuxDialog implements Dialog {
    public void render() {
        System.out.println("Rendering Linux-style dialog");
    }
    
    public void onClick() {
        System.out.println("Linux dialog click handled");
    }
}

// Creator abstract class
abstract class Application {
    // Factory method - subclasses will implement this
    protected abstract Dialog createDialog();
    
    // Template method that uses the factory method
    public void showDialog() {
        Dialog dialog = createDialog();
        dialog.render();
        dialog.onClick();
    }
}

// Concrete creators
class WindowsApplication extends Application {
    protected Dialog createDialog() {
        return new WindowsDialog();
    }
}

class MacApplication extends Application {
    protected Dialog createDialog() {
        return new MacDialog();
    }
}

class LinuxApplication extends Application {
    protected Dialog createDialog() {
        return new LinuxDialog();
    }
}

// Client code
public class ApplicationRunner {
    private static Application configureApplication() {
        String osName = System.getProperty("os.name").toLowerCase();
        
        if (osName.contains("win")) {
            return new WindowsApplication();
        } else if (osName.contains("mac")) {
            return new MacApplication();
        } else {
            return new LinuxApplication();
        }
    }
    
    public static void main(String[] args) {
        Application app = configureApplication();
        app.showDialog();
    }
}

3. Abstract Factory Pattern (GoF Pattern)

The Abstract Factory provides an interface for creating families of related objects.

// Abstract products
interface Button {
    void click();
    void render();
}

interface Checkbox {
    void check();
    void render();
}

interface TextField {
    void type(String text);
    void render();
}

// Windows family
class WindowsButton implements Button {
    public void click() { System.out.println("Windows button clicked"); }
    public void render() { System.out.println("Rendering Windows button"); }
}

class WindowsCheckbox implements Checkbox {
    public void check() { System.out.println("Windows checkbox checked"); }
    public void render() { System.out.println("Rendering Windows checkbox"); }
}

class WindowsTextField implements TextField {
    public void type(String text) { System.out.println("Typing in Windows field: " + text); }
    public void render() { System.out.println("Rendering Windows text field"); }
}

// Mac family
class MacButton implements Button {
    public void click() { System.out.println("Mac button clicked"); }
    public void render() { System.out.println("Rendering Mac button"); }
}

class MacCheckbox implements Checkbox {
    public void check() { System.out.println("Mac checkbox checked"); }
    public void render() { System.out.println("Rendering Mac checkbox"); }
}

class MacTextField implements TextField {
    public void type(String text) { System.out.println("Typing in Mac field: " + text); }
    public void render() { System.out.println("Rendering Mac text field"); }
}

// Abstract factory
interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
    TextField createTextField();
}

// Concrete factories
class WindowsFactory implements GUIFactory {
    public Button createButton() { return new WindowsButton(); }
    public Checkbox createCheckbox() { return new WindowsCheckbox(); }
    public TextField createTextField() { return new WindowsTextField(); }
}

class MacFactory implements GUIFactory {
    public Button createButton() { return new MacButton(); }
    public Checkbox createCheckbox() { return new MacCheckbox(); }
    public TextField createTextField() { return new MacTextField(); }
}

// Client code
class Application {
    private GUIFactory factory;
    private Button button;
    private Checkbox checkbox;
    private TextField textField;
    
    public Application(GUIFactory factory) {
        this.factory = factory;
    }
    
    public void createUI() {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
        textField = factory.createTextField();
    }
    
    public void renderUI() {
        button.render();
        checkbox.render();
        textField.render();
    }
}

Practical Applications

Database Connection Factory

from abc import ABC, abstractmethod
import sqlite3
import psycopg2
from typing import Any

# Product interface
class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self) -> Any:
        pass
    
    @abstractmethod
    def execute_query(self, query: str) -> Any:
        pass
    
    @abstractmethod
    def close(self) -> None:
        pass

# Concrete products
class SQLiteConnection(DatabaseConnection):
    def __init__(self, database_path: str):
        self.database_path = database_path
        self.connection = None
    
    def connect(self):
        self.connection = sqlite3.connect(self.database_path)
        print(f"Connected to SQLite database: {self.database_path}")
        return self.connection
    
    def execute_query(self, query: str):
        if not self.connection:
            self.connect()
        cursor = self.connection.cursor()
        cursor.execute(query)
        return cursor.fetchall()
    
    def close(self):
        if self.connection:
            self.connection.close()
            print("SQLite connection closed")

class PostgreSQLConnection(DatabaseConnection):
    def __init__(self, host: str, port: int, database: str, user: str, password: str):
        self.host = host
        self.port = port
        self.database = database
        self.user = user
        self.password = password
        self.connection = None
    
    def connect(self):
        self.connection = psycopg2.connect(
            host=self.host,
            port=self.port,
            database=self.database,
            user=self.user,
            password=self.password
        )
        print(f"Connected to PostgreSQL database: {self.database}")
        return self.connection
    
    def execute_query(self, query: str):
        if not self.connection:
            self.connect()
        cursor = self.connection.cursor()
        cursor.execute(query)
        return cursor.fetchall()
    
    def close(self):
        if self.connection:
            self.connection.close()
            print("PostgreSQL connection closed")

# Factory
class DatabaseConnectionFactory:
    @staticmethod
    def create_connection(db_type: str, **kwargs) -> DatabaseConnection:
        if db_type.lower() == 'sqlite':
            return SQLiteConnection(kwargs.get('database_path', 'default.db'))
        elif db_type.lower() == 'postgresql':
            return PostgreSQLConnection(
                host=kwargs.get('host', 'localhost'),
                port=kwargs.get('port', 5432),
                database=kwargs.get('database', 'postgres'),
                user=kwargs.get('user', 'postgres'),
                password=kwargs.get('password', '')
            )
        else:
            raise ValueError(f"Unsupported database type: {db_type}")

# Usage
def demonstrate_database_factory():
    # Create SQLite connection
    sqlite_conn = DatabaseConnectionFactory.create_connection(
        'sqlite', 
        database_path='app.db'
    )
    sqlite_conn.connect()
    sqlite_conn.execute_query("SELECT * FROM users")
    sqlite_conn.close()
    
    # Create PostgreSQL connection
    postgres_conn = DatabaseConnectionFactory.create_connection(
        'postgresql',
        host='localhost',
        database='myapp',
        user='dbuser',
        password='secret'
    )
    postgres_conn.connect()
    postgres_conn.execute_query("SELECT * FROM customers")
    postgres_conn.close()

Common Misconceptions

  1. Misconception: Factory patterns are only useful for complex object creation Reality: They’re valuable any time you want to decouple client code from concrete classes

  2. Misconception: Simple Factory is always inferior to Factory Method Reality: Simple Factory is often sufficient for straightforward scenarios and easier to understand

  3. Misconception: Abstract Factory is always the most flexible option Reality: Abstract Factory adds complexity and should only be used when you need families of related objects

5. Live Coding Demonstration (20-25 minutes)

Example 1: Payment Processing System

// Payment processor interface
class PaymentProcessor {
    process(amount) {
        throw new Error("Method must be implemented");
    }
    
    validate(paymentData) {
        throw new Error("Method must be implemented");
    }
}

// Concrete payment processors
class CreditCardProcessor extends PaymentProcessor {
    process(amount) {
        console.log(`Processing $${amount} via Credit Card`);
        // Simulate credit card processing
        return {
            success: true,
            transactionId: `CC_${Date.now()}`,
            method: 'Credit Card'
        };
    }
    
    validate(paymentData) {
        return paymentData.cardNumber && paymentData.expiryDate && paymentData.cvv;
    }
}

class PayPalProcessor extends PaymentProcessor {
    process(amount) {
        console.log(`Processing $${amount} via PayPal`);
        // Simulate PayPal processing
        return {
            success: true,
            transactionId: `PP_${Date.now()}`,
            method: 'PayPal'
        };
    }
    
    validate(paymentData) {
        return paymentData.email && paymentData.password;
    }
}

class CryptoProcessor extends PaymentProcessor {
    process(amount) {
        console.log(`Processing $${amount} via Cryptocurrency`);
        // Simulate crypto processing
        return {
            success: true,
            transactionId: `CRYPTO_${Date.now()}`,
            method: 'Cryptocurrency'
        };
    }
    
    validate(paymentData) {
        return paymentData.walletAddress && paymentData.privateKey;
    }
}

// Factory with enum for better type safety
const PaymentType = {
    CREDIT_CARD: 'CREDIT_CARD',
    PAYPAL: 'PAYPAL',
    CRYPTO: 'CRYPTO'
};

class PaymentProcessorFactory {
    static create(type) {
        switch (type) {
            case PaymentType.CREDIT_CARD:
                return new CreditCardProcessor();
            case PaymentType.PAYPAL:
                return new PayPalProcessor();
            case PaymentType.CRYPTO:
                return new CryptoProcessor();
            default:
                throw new Error(`Unsupported payment type: ${type}`);
        }
    }
    
    static getSupportedTypes() {
        return Object.values(PaymentType);
    }
}

// Payment service using the factory
class PaymentService {
    static processPayment(type, amount, paymentData) {
        try {
            const processor = PaymentProcessorFactory.create(type);
            
            if (!processor.validate(paymentData)) {
                throw new Error('Invalid payment data');
            }
            
            const result = processor.process(amount);
            console.log('Payment processed successfully:', result);
            return result;
        } catch (error) {
            console.error('Payment processing failed:', error.message);
            return { success: false, error: error.message };
        }
    }
}

// Demonstration
console.log('=== Payment Processing Demo ===');

// Process different payment types
PaymentService.processPayment(PaymentType.CREDIT_CARD, 100.00, {
    cardNumber: '1234-5678-9012-3456',
    expiryDate: '12/25',
    cvv: '123'
});

PaymentService.processPayment(PaymentType.PAYPAL, 250.00, {
    email: 'user@example.com',
    password: 'secret123'
});

PaymentService.processPayment(PaymentType.CRYPTO, 500.00, {
    walletAddress: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
    privateKey: 'private_key_here'
});

// Show supported types
console.log('Supported payment types:', PaymentProcessorFactory.getSupportedTypes());

Example 2: Document Export Factory Method

from abc import ABC, abstractmethod
from datetime import datetime
import json
import csv

# Product interface
class DocumentExporter(ABC):
    @abstractmethod
    def export(self, data: dict) -> str:
        pass
    
    @abstractmethod
    def get_file_extension(self) -> str:
        pass

# Concrete products
class JSONExporter(DocumentExporter):
    def export(self, data: dict) -> str:
        return json.dumps(data, indent=2, default=str)
    
    def get_file_extension(self) -> str:
        return '.json'

class CSVExporter(DocumentExporter):
    def export(self, data: dict) -> str:
        if not data:
            return ""
        
        # Convert dict to CSV format
        output = []
        if isinstance(list(data.values())[0], list):
            # Handle multiple records
            keys = data.keys()
            output.append(','.join(keys))
            
            max_len = max(len(v) for v in data.values())
            for i in range(max_len):
                row = []
                for key in keys:
                    value = data[key][i] if i < len(data[key]) else ""
                    row.append(str(value))
                output.append(','.join(row))
        else:
            # Handle single record
            output.append(','.join(data.keys()))
            output.append(','.join(str(v) for v in data.values()))
        
        return '\n'.join(output)
    
    def get_file_extension(self) -> str:
        return '.csv'

class XMLExporter(DocumentExporter):
    def export(self, data: dict) -> str:
        def dict_to_xml(d, root_name='root'):
            xml = f'<{root_name}>\n'
            for key, value in d.items():
                if isinstance(value, dict):
                    xml += f'  <{key}>\n'
                    for k, v in value.items():
                        xml += f'    <{k}>{v}</{k}>\n'
                    xml += f'  </{key}>\n'
                elif isinstance(value, list):
                    xml += f'  <{key}>\n'
                    for item in value:
                        xml += f'    <item>{item}</item>\n'
                    xml += f'  </{key}>\n'
                else:
                    xml += f'  <{key}>{value}</{key}>\n'
            xml += f'</{root_name}>'
            return xml
        
        return dict_to_xml(data)
    
    def get_file_extension(self) -> str:
        return '.xml'

# Creator abstract class (Factory Method pattern)
class DocumentProcessor(ABC):
    @abstractmethod
    def create_exporter(self) -> DocumentExporter:
        pass
    
    def process_and_export(self, data: dict, filename: str) -> str:
        exporter = self.create_exporter()
        exported_content = exporter.export(data)
        
        full_filename = filename + exporter.get_file_extension()
        
        # Simulate file writing
        print(f"Exporting to {full_filename}")
        print("Content preview:")
        print(exported_content[:200] + "..." if len(exported_content) > 200 else exported_content)
        print("-" * 50)
        
        return exported_content

# Concrete creators
class JSONProcessor(DocumentProcessor):
    def create_exporter(self) -> DocumentExporter:
        return JSONExporter()

class CSVProcessor(DocumentProcessor):
    def create_exporter(self) -> DocumentExporter:
        return CSVExporter()

class XMLProcessor(DocumentProcessor):
    def create_exporter(self) -> DocumentExporter:
        return XMLExporter()

# Factory for processors
class ProcessorFactory:
    @staticmethod
    def create_processor(format_type: str) -> DocumentProcessor:
        processors = {
            'json': JSONProcessor,
            'csv': CSVProcessor,
            'xml': XMLProcessor
        }
        
        processor_class = processors.get(format_type.lower())
        if not processor_class:
            raise ValueError(f"Unsupported format: {format_type}")
        
        return processor_class()

# Demonstration
def demonstrate_document_export():
    # Sample data
    sample_data = {
        'employees': [
            {'name': 'John Doe', 'dept': 'Engineering', 'salary': 85000},
            {'name': 'Jane Smith', 'dept': 'Marketing', 'salary': 75000},
            {'name': 'Bob Johnson', 'dept': 'Sales', 'salary': 65000}
        ],
        'company': 'TechCorp Inc.',
        'export_date': datetime.now()
    }
    
    formats = ['json', 'csv', 'xml']
    
    for format_type in formats:
        try:
            processor = ProcessorFactory.create_processor(format_type)
            processor.process_and_export(sample_data, f'employee_report')
        except ValueError as e:
            print(f"Error: {e}")

# Run demonstration
demonstrate_document_export()

Debugging Demonstration

Show common errors and how to fix them:

// Common mistake: Forgetting to handle new types
public class IncompleteFactory {
    public static PaymentProcessor create(String type) {
        if (type.equals("CREDIT_CARD")) {
            return new CreditCardProcessor();
        } else if (type.equals("PAYPAL")) {
            return new PayPalProcessor();
        }
        // Bug: No handling for new types like "CRYPTO"
        return null; // This will cause NullPointerException!
    }
}

// Better approach: Throw exception for unknown types
public class BetterFactory {
    public static PaymentProcessor create(String type) {
        switch (type) {
            case "CREDIT_CARD":
                return new CreditCardProcessor();
            case "PAYPAL":
                return new PayPalProcessor();
            case "CRYPTO":
                return new CryptoProcessor();
            default:
                throw new IllegalArgumentException("Unknown payment type: " + type);
        }
    }
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Basic Practice (10 minutes)

Objective: Create a Simple Factory for different types of notifications

Instructions:

  1. Create a Notification interface with send(String message) method
  2. Implement concrete classes: EmailNotification, SMSNotification, PushNotification
  3. Create a NotificationFactory with a static method to create notifications
  4. Test the factory with different notification types

Starter Code:

interface Notification {
    void send(String message);
}

class EmailNotification implements Notification {
    // TODO: Implement send method
}

class SMSNotification implements Notification {
    // TODO: Implement send method
}

class PushNotification implements Notification {
    // TODO: Implement send method
}

class NotificationFactory {
    // TODO: Implement factory method
    public static Notification createNotification(String type) {
        // TODO: Return appropriate notification type
        return null;
    }
}

Solution:

interface Notification {
    void send(String message);
}

class EmailNotification implements Notification {
    public void send(String message) {
        System.out.println("Email sent: " + message);
    }
}

class SMSNotification implements Notification {
    public void send(String message) {
        System.out.println("SMS sent: " + message);
    }
}

class PushNotification implements Notification {
    public void send(String message) {
        System.out.println("Push notification sent: " + message);
    }
}

class NotificationFactory {
    public enum NotificationType {
        EMAIL, SMS, PUSH
    }
    
    public static Notification createNotification(NotificationType type) {
        switch (type) {
            case EMAIL:
                return new EmailNotification();
            case SMS:
                return new SMSNotification();
            case PUSH:
                return new PushNotification();
            default:
                throw new IllegalArgumentException("Unknown notification type: " + type);
        }
    }
}

Exercise 2: Intermediate Challenge (15 minutes)

Objective: Implement Factory Method pattern for different logging systems

Requirements:

  • Create abstract Logger class with abstract createLogHandler() method
  • Implement concrete loggers: FileLogger, DatabaseLogger, ConsoleLogger
  • Each logger should create appropriate handler (FileHandler, DatabaseHandler, ConsoleHandler)
  • Add log levels (INFO, WARNING, ERROR) and formatting

Challenge Features:

  • Add timestamp formatting
  • Implement log filtering by level
  • Create configuration for each logger type

Exercise 3: Creative Application (15 minutes)

Objective: Design an Abstract Factory for different game environments

Challenge: Create an Abstract Factory that produces:

  • Different enemy types (Zombie, Alien, Robot)
  • Different weapons (Pistol, Rifle, Laser)
  • Different power-ups (Health, Shield, Speed)

Game Environments:

  • Medieval (produces medieval-themed variants)
  • SciFi (produces futuristic variants)
  • Modern (produces contemporary variants)

Extension Ideas:

  • Add difficulty levels that affect enemy strength
  • Implement weapon upgrade systems
  • Create environment-specific sound effects and graphics

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Not handling unknown types in factory methods Cause: Forgetting to add proper error handling for invalid input Solution: Always throw meaningful exceptions for unsupported types

  2. Problem: Violating Open/Closed Principle with Simple Factory Cause: Adding new types requires modifying the factory class Solution: Use Factory Method or Abstract Factory for better extensibility

  3. Problem: Creating too many factory classes Cause: Overusing patterns when simple constructors would suffice Solution: Only use factories when you need decoupling or complex creation logic

  4. Problem: Not making factory methods thread-safe Cause: Shared state in factory without proper synchronization Solution: Use stateless factories or proper synchronization mechanisms

Debugging Tips

  • Log factory method calls to trace object creation patterns
  • Use unit tests to verify correct object types are created
  • Test edge cases like null inputs and invalid type parameters
  • Monitor memory usage if factories cache created objects

When to Use Each Factory Type

  • Simple Factory: When you have a few concrete classes and simple creation logic
  • Factory Method: When you want subclasses to control object creation
  • Abstract Factory: When you need to create families of related objects

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Use enums instead of strings for type parameters to avoid typos and enable compile-time checking
  • Return interfaces, not concrete classes from factory methods for better abstraction
  • Consider using dependency injection frameworks for more sophisticated object creation
  • Document the lifecycle of objects created by factories

Next Steps in Your Learning Journey

  • Study Builder pattern for complex object construction with many optional parameters
  • Learn Prototype pattern for creating objects by cloning existing instances
  • Explore Dependency Injection containers as modern alternatives to factories
  • Investigate Service Locator pattern for service discovery and creation

Modern Alternatives and Considerations

  • Dependency Injection: Spring, Guice, Dagger provide more sophisticated object creation
  • Functional Approaches: Higher-order functions can sometimes replace factory patterns
  • Configuration-driven Factories: Use external configuration to determine object types
  • Plugin Architectures: Dynamic loading of implementations at runtime

9. Summary and Review (5 minutes)

Key Takeaways

  1. Factory patterns decouple object creation from client code, improving flexibility
  2. Choose the right factory type based on complexity: Simple Factory → Factory Method → Abstract Factory
  3. Always handle invalid inputs with meaningful error messages
  4. Use factories when you need polymorphism, decoupling, or complex object creation logic

Quick Review Questions

  1. What’s the main difference between Factory Method and Abstract Factory patterns?
  2. When would you choose Simple Factory over Factory Method pattern?
  3. How do Factory patterns support the Open/Closed Principle?

For Instructors

Teaching Tips

  • Start with Simple Factory as it’s easiest to understand, then progress to more complex variants
  • Use real-world analogies (restaurants, manufacturing) to explain the concept
  • Show refactoring examples where factory patterns improve existing code
  • Emphasize when NOT to use factories to avoid over-engineering

Assessment Ideas

  • Code refactoring exercise: Transform tightly-coupled code to use factory patterns
  • Design challenge: Choose appropriate factory type for given requirements
  • Implementation review: Identify and fix problems in factory implementations

Adaptation Notes

  • Beginners: Focus on Simple Factory and basic Factory Method usage
  • Advanced: Discuss performance implications, memory management, and integration with DI frameworks
  • Language-specific: Show language-specific features like Python’s __new__ or JavaScript’s prototype system