Learning Objectives

  • Understand the Factory pattern concept
  • Implement Simple Factory and Factory Method
  • Use factories for dependency injection
  • Create plugin systems with factories
  • Apply best practices and patterns

What is the Factory Pattern?

A Factory creates objects based on input or conditions, hiding the instantiation logic from the client code. Instead of using new directly, you call a factory method that decides which class to instantiate.

Simple Factory

class User {
    constructor(name, role) {
        this.name = name;
        this.role = role;
    }
    
    getPermissions() {
        return [];
    }
}

class Admin extends User {
    constructor(name) {
        super(name, 'admin');
    }
    
    getPermissions() {
        return ['read', 'write', 'delete', 'manage'];
    }
}

class Editor extends User {
    constructor(name) {
        super(name, 'editor');
    }
    
    getPermissions() {
        return ['read', 'write'];
    }
}

class Guest extends User {
    constructor(name) {
        super(name, 'guest');
    }
    
    getPermissions() {
        return ['read'];
    }
}

class UserFactory {
    static createUser(type, name) {
        switch (type) {
            case 'admin':
                return new Admin(name);
            case 'editor':
                return new Editor(name);
            case 'guest':
                return new Guest(name);
            default:
                return new User(name, 'user');
        }
    }
}

// Usage
const admin = UserFactory.createUser('admin', 'John');
console.log(admin.getPermissions()); // ['read', 'write', 'delete', 'manage']

const guest = UserFactory.createUser('guest', 'Jane');
console.log(guest.getPermissions()); // ['read']

Factory Method Pattern

Uses inheritance to delegate object creation to subclasses:

class VehicleFactory {
    createVehicle() {
        throw new Error('createVehicle must be implemented');
    }
    
    deliverVehicle(model) {
        const vehicle = this.createVehicle(model);
        vehicle.assemble();
        vehicle.test();
        return vehicle;
    }
}

class CarFactory extends VehicleFactory {
    createVehicle(model) {
        return new Car(model);
    }
}

class BikeFactory extends VehicleFactory {
    createVehicle(model) {
        return new Bike(model);
    }
}

class Car {
    constructor(model) {
        this.model = model;
        this.type = 'car';
    }
    
    assemble() {
        console.log(`Assembling ${this.model} car`);
    }
    
    test() {
        console.log(`Testing ${this.model} car`);
    }
}

class Bike {
    constructor(model) {
        this.model = model;
        this.type = 'bike';
    }
    
    assemble() {
        console.log(`Assembling ${this.model} bike`);
    }
    
    test() {
        console.log(`Testing ${this.model} bike`);
    }
}

// Usage
const carFactory = new CarFactory();
const car = carFactory.deliverVehicle('Tesla Model 3');

const bikeFactory = new BikeFactory();
const bike = bikeFactory.deliverVehicle('Harley Davidson');

Real-World Examples

HTTP Client Factory

class HttpClientFactory {
    static create(type = 'fetch') {
        switch (type) {
            case 'fetch':
                return new FetchClient();
            case 'axios':
                return new AxiosClient();
            case 'xhr':
                return new XHRClient();
            default:
                throw new Error(`Unknown client type: ${type}`);
        }
    }
}

class FetchClient {
    async get(url) {
        const response = await fetch(url);
        return response.json();
    }
    
    async post(url, data) {
        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
        return response.json();
    }
}

class AxiosClient {
    async get(url) {
        const response = await axios.get(url);
        return response.data;
    }
    
    async post(url, data) {
        const response = await axios.post(url, data);
        return response.data;
    }
}

// Usage
const client = HttpClientFactory.create('fetch');
const data = await client.get('/api/users');

UI Component Factory

class ComponentFactory {
    static create(type, props) {
        const components = {
            button: () => new Button(props),
            input: () => new Input(props),
            select: () => new Select(props),
            checkbox: () => new Checkbox(props)
        };
        
        const creator = components[type];
        if (!creator) {
            throw new Error(`Unknown component type: ${type}`);
        }
        
        return creator();
    }
}

class Button {
    constructor({ text, onClick, variant = 'primary' }) {
        this.text = text;
        this.onClick = onClick;
        this.variant = variant;
    }
    
    render() {
        const button = document.createElement('button');
        button.textContent = this.text;
        button.className = `btn btn-${this.variant}`;
        button.addEventListener('click', this.onClick);
        return button;
    }
}

class Input {
    constructor({ placeholder, type = 'text', onChange }) {
        this.placeholder = placeholder;
        this.type = type;
        this.onChange = onChange;
    }
    
    render() {
        const input = document.createElement('input');
        input.type = this.type;
        input.placeholder = this.placeholder;
        input.addEventListener('input', this.onChange);
        return input;
    }
}

// Usage
const button = ComponentFactory.create('button', {
    text: 'Click me',
    onClick: () => console.log('Clicked!'),
    variant: 'success'
});

document.body.appendChild(button.render());

Database Connection Factory

class DatabaseFactory {
    static createConnection(type, config) {
        const connections = {
            mysql: () => new MySQLConnection(config),
            postgres: () => new PostgresConnection(config),
            mongodb: () => new MongoConnection(config),
            sqlite: () => new SQLiteConnection(config)
        };
        
        const creator = connections[type];
        if (!creator) {
            throw new Error(`Unknown database type: ${type}`);
        }
        
        return creator();
    }
}

class PostgresConnection {
    constructor(config) {
        this.config = config;
        this.client = null;
    }
    
    async connect() {
        // Connect to PostgreSQL
        console.log(`Connecting to PostgreSQL at ${this.config.host}`);
    }
    
    async query(sql) {
        return `PostgreSQL: ${sql}`;
    }
}

class MongoConnection {
    constructor(config) {
        this.config = config;
        this.client = null;
    }
    
    async connect() {
        // Connect to MongoDB
        console.log(`Connecting to MongoDB at ${this.config.host}`);
    }
    
    async query(collection, filter) {
        return `MongoDB: ${collection} ${JSON.stringify(filter)}`;
    }
}

// Usage
const db = DatabaseFactory.createConnection('postgres', {
    host: 'localhost',
    port: 5432,
    database: 'myapp'
});

await db.connect();

Abstract Factory

Create families of related objects:

class UIFactory {
    createButton() {
        throw new Error('Must implement createButton');
    }
    
    createInput() {
        throw new Error('Must implement createInput');
    }
    
    createCard() {
        throw new Error('Must implement createCard');
    }
}

class DarkThemeFactory extends UIFactory {
    createButton() {
        return new DarkButton();
    }
    
    createInput() {
        return new DarkInput();
    }
    
    createCard() {
        return new DarkCard();
    }
}

class LightThemeFactory extends UIFactory {
    createButton() {
        return new LightButton();
    }
    
    createInput() {
        return new LightInput();
    }
    
    createCard() {
        return new LightCard();
    }
}

// Usage
const theme = 'dark';
const factory = theme === 'dark' 
    ? new DarkThemeFactory() 
    : new LightThemeFactory();

const button = factory.createButton();
const input = factory.createInput();
const card = factory.createCard();

// All components match the theme

When to Use Factory Pattern

Good use cases:
  • Object creation is complex
  • Need to decouple creation from usage
  • Creating families of related objects
  • Plugin systems
  • Dependency injection
  • Configuration-based object creation

Benefits

Best Practices

1. Use object lookup instead of switch
const creators = {
    admin: () => new Admin(),
    user: () => new User()
};

return creators[type]();
2. Validate input
static create(type) {
    if (!this.creators[type]) {
        throw new Error(`Unknown type: ${type}`);
    }
    return this.creators[type]();
}
3. Use dependency injection
class ServiceFactory {
    constructor(dependencies) {
        this.deps = dependencies;
    }
    
    create(type) {
        return new Service(type, this.deps);
    }
}

Key Takeaways