Factory Pattern
The Factory pattern provides an interface for creating objects without specifying their exact classes. This pattern is particularly valuable in legacy systems for managing object creation complexity, enabling dependency injection, and making systems more testable and flexible.
Video Summary
The video presentation explores the Factory pattern:
- What factories are and why they're useful
- Different types: Simple Factory, Factory Method, Abstract Factory
- Using factories to manage object creation complexity
- Enabling dependency injection and testability
- Real-world examples in legacy modernization
- When to use factories vs. direct instantiation
Key Concepts
1. Encapsulating Object Creation
Instead of scattering new keywords throughout code, centralize creation logic:
// Without factory: creation logic scattered
const user = new User(data);
const premium = new PremiumUser(data);
const admin = new AdminUser(data);
// With factory: centralized creation
const user = UserFactory.create(data);
2. Types of Factories
Simple Factory: Single method creates objects
class UserFactory {
static create(type: string, data: UserData): User {
switch (type) {
case 'admin':
return new AdminUser(data);
case 'premium':
return new PremiumUser(data);
default:
return new RegularUser(data);
}
}
}
Factory Method: Subclasses decide which class to instantiate
abstract class UserRepository {
abstract createUser(): User;
saveUser(data: UserData): User {
const user = this.createUser();
user.populate(data);
return user;
}
}
class AdminRepository extends UserRepository {
createUser(): User {
return new AdminUser();
}
}
class RegularRepository extends UserRepository {
createUser(): User {
return new RegularUser();
}
}
Abstract Factory: Creates families of related objects
interface DatabaseFactory {
createConnection(): Connection;
createQueryBuilder(): QueryBuilder;
createTransaction(): Transaction;
}
class MySQLFactory implements DatabaseFactory {
createConnection(): Connection {
return new MySQLConnection();
}
createQueryBuilder(): QueryBuilder {
return new MySQLQueryBuilder();
}
createTransaction(): Transaction {
return new MySQLTransaction();
}
}
class PostgreSQLFactory implements DatabaseFactory {
createConnection(): Connection {
return new PostgreSQLConnection();
}
createQueryBuilder(): QueryBuilder {
return new PostgreSQLQueryBuilder();
}
createTransaction(): Transaction {
return new PostgreSQLTransaction();
}
}
Real-World Applications
Example 1: Payment Processor Factory
interface PaymentProcessor {
processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult>;
}
class StripeProcessor implements PaymentProcessor {
async processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult> {
// Stripe-specific logic
}
}
class PayPalProcessor implements PaymentProcessor {
async processPayment(amount: number, details: PaymentDetails): Promise<PaymentResult> {
// PayPal-specific logic
}
}
class PaymentProcessorFactory {
static create(method: string): PaymentProcessor {
switch (method) {
case 'stripe':
return new StripeProcessor();
case 'paypal':
return new PayPalProcessor();
default:
throw new Error(`Unknown payment method: ${method}`);
}
}
}
// Usage
const processor = PaymentProcessorFactory.create(order.paymentMethod);
await processor.processPayment(order.total, order.paymentDetails);
Example 2: Database Connection Factory
class DatabaseFactory {
static createConnection(config: DatabaseConfig): Database {
switch (config.type) {
case 'mysql':
return new MySQLDatabase(config);
case 'postgres':
return new PostgresDatabase(config);
case 'mongodb':
return new MongoDatabase(config);
default:
throw new Error(`Unsupported database: ${config.type}`);
}
}
}
// Configuration-driven
const db = DatabaseFactory.createConnection({
type: process.env.DB_TYPE,
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
});
Example 3: Report Generator Factory
interface ReportGenerator {
generate(data: any): string;
}
class PDFReportGenerator implements ReportGenerator {
generate(data: any): string {
// Generate PDF
return 'pdf-content';
}
}
class ExcelReportGenerator implements ReportGenerator {
generate(data: any): string {
// Generate Excel
return 'excel-content';
}
}
class CSVReportGenerator implements ReportGenerator {
generate(data: any): string {
// Generate CSV
return 'csv-content';
}
}
class ReportFactory {
static create(format: string): ReportGenerator {
const generators = {
pdf: PDFReportGenerator,
excel: ExcelReportGenerator,
csv: CSVReportGenerator,
};
const GeneratorClass = generators[format];
if (!GeneratorClass) {
throw new Error(`Unknown report format: ${format}`);
}
return new GeneratorClass();
}
}
// Usage
app.get('/reports/:format', async (req, res) => {
const generator = ReportFactory.create(req.params.format);
const report = generator.generate(await fetchData());
res.send(report);
});
Example 4: Notification Service Factory
interface NotificationService {
send(to: string, message: string): Promise<void>;
}
class EmailNotification implements NotificationService {
async send(to: string, message: string): Promise<void> {
// Send email
}
}
class SMSNotification implements NotificationService {
async send(to: string, message: string): Promise<void> {
// Send SMS
}
}
class PushNotification implements NotificationService {
async send(to: string, message: string): Promise<void> {
// Send push notification
}
}
class NotificationFactory {
private static services = new Map<string, NotificationService>();
static register(type: string, service: NotificationService): void {
this.services.set(type, service);
}
static create(type: string): NotificationService {
const service = this.services.get(type);
if (!service) {
throw new Error(`No notification service registered for: ${type}`);
}
return service;
}
}
// Setup
NotificationFactory.register('email', new EmailNotification());
NotificationFactory.register('sms', new SMSNotification());
NotificationFactory.register('push', new PushNotification());
// Usage
const service = NotificationFactory.create(user.preferredNotificationMethod);
await service.send(user.contact, 'Your order has shipped');
Example 5: Legacy System Adapter Factory
// Abstract interface
interface OrderService {
createOrder(order: Order): Promise<OrderResult>;
}
// Legacy SOAP service adapter
class LegacySOAPOrderService implements OrderService {
async createOrder(order: Order): Promise<OrderResult> {
// Convert to SOAP format, call legacy system
}
}
// Modern REST service
class ModernRESTOrderService implements OrderService {
async createOrder(order: Order): Promise<OrderResult> {
// Call modern REST API
}
}
class OrderServiceFactory {
static create(): OrderService {
// Use feature flag to control which implementation
if (featureFlags.isEnabled('modern-order-service')) {
return new ModernRESTOrderService();
}
return new LegacySOAPOrderService();
}
}
// Application code doesn't know which implementation is used
const orderService = OrderServiceFactory.create();
await orderService.createOrder(order);
Testing Benefits
Before: Hard to Test
class OrderProcessor {
processOrder(order: Order): void {
// Hard-coded dependency
const paymentProcessor = new StripeProcessor();
paymentProcessor.processPayment(order.total, order.paymentDetails);
}
}
// Can't easily test without hitting Stripe
After: Easy to Test
class OrderProcessor {
constructor(private processorFactory: PaymentProcessorFactory) {}
processOrder(order: Order): void {
const processor = this.processorFactory.create(order.paymentMethod);
processor.processPayment(order.total, order.paymentDetails);
}
}
// Testing
class MockProcessorFactory {
create(method: string): PaymentProcessor {
return new MockPaymentProcessor();
}
}
const processor = new OrderProcessor(new MockProcessorFactory());
Common Pitfalls
1. God Factory
Problem: Factory that knows about too many classes.
// Bad: Factory knows everything
class ObjectFactory {
create(type: string): any {
// 50 different object types
switch (type) {
case 'user': return new User();
case 'order': return new Order();
case 'product': return new Product();
// ... 47 more cases
}
}
}
Solution: Multiple focused factories:
class UserFactory { /* creates users */ }
class OrderFactory { /* creates orders */ }
class ProductFactory { /* creates products */ }
2. Premature Abstraction
Problem: Creating factories when direct instantiation is fine.
// Overkill for simple case
class StringFactory {
static create(value: string): string {
return new String(value).toString();
}
}
// Just use:
const str = 'hello';
Rule of thumb: Use factories when:
- Creation logic is complex
- Need to switch implementations
- Want to control instantiation
3. Not Using Dependency Injection
Problem: Factory calls scattered throughout code.
// Everywhere in code
const db = DatabaseFactory.create(config);
Solution: Create once, inject everywhere:
// In main.ts
const db = DatabaseFactory.create(config);
const app = new App(db);
// Inject to classes
class UserService {
constructor(private db: Database) {}
}
Key Takeaways
- Factory pattern encapsulates object creation logic
- Three types: Simple Factory, Factory Method, Abstract Factory
- Useful for complex creation, switching implementations, testing
- Enables dependency injection and loose coupling
- Particularly valuable in legacy systems for migration
- Don't overuse—simple cases don't need factories
- Combine with dependency injection for maximum flexibility
Further Reading
- Design Patterns - Gang of Four factory patterns
- Dependency Injection Principles - Using factories with DI
- Refactoring - Replace Constructor with Factory Method