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
- Loose coupling: Client code doesn't depend on concrete classes
- Single Responsibility: Creation logic in one place
- Open/Closed: Easy to add new types without changing existing code
- Easy to extend: Add new products by adding new classes
- Centralized: All creation logic in factory
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
- Factory creates objects without specifying exact classes
- Simple Factory uses switch/if or object lookup
- Factory Method uses inheritance
- Abstract Factory creates families of objects
- Perfect for plugin systems and dependency injection
- Promotes loose coupling and extensibility
- Centralizes object creation logic
- Follows Open/Closed Principle