Factory Method Pattern in C#: Flexible Object Creation
The Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. Instead of calling a constructor directly, you call a factory method that returns the object.
What Is the Factory Method Pattern?
The Factory Method pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. It lets a class defer instantiation to subclasses, promoting loose coupling and adherence to the Open/Closed Principle.
// Product interface
public interface IProduct
{
string GetName();
void Display();
}
// Concrete Products
public class ConcreteProductA : IProduct
{
public string GetName() => "Product A";
public void Display() => Console.WriteLine("This is Product A");
}
public class ConcreteProductB : IProduct
{
public string GetName() => "Product B";
public void Display() => Console.WriteLine("This is Product B");
}
// Creator (Factory)
public abstract class Creator
{
// Factory Method
public abstract IProduct CreateProduct();
// Business logic that uses the factory method
public void DoSomething()
{
IProduct product = CreateProduct();
Console.WriteLine($"Working with {product.GetName()}");
product.Display();
}
}
// Concrete Creators
public class ConcreteCreatorA : Creator
{
public override IProduct CreateProduct()
{
return new ConcreteProductA();
}
}
public class ConcreteCreatorB : Creator
{
public override IProduct CreateProduct()
{
return new ConcreteProductB();
}
}
// Usage
Creator creator = new ConcreteCreatorA();
creator.DoSomething(); // Output: Working with Product A
Why Use the Factory Method Pattern?
1. Decoupling Object Creation
The pattern separates object creation from object usage, reducing dependencies:
// Without Factory Method - tight coupling
public class OrderProcessor
{
public void ProcessOrder(string type)
{
IShipping shipping;
// Direct instantiation creates tight coupling
if (type == "air")
shipping = new AirShipping();
else if (type == "ground")
shipping = new GroundShipping();
else
shipping = new SeaShipping();
shipping.Ship();
}
}
// With Factory Method - loose coupling
public abstract class ShippingFactory
{
public abstract IShipping CreateShipping();
public void ProcessShipment()
{
IShipping shipping = CreateShipping();
shipping.Ship();
}
}
public class AirShippingFactory : ShippingFactory
{
public override IShipping CreateShipping() => new AirShipping();
}
2. Open/Closed Principle
Add new product types without modifying existing code:
// Adding a new product type doesn't require changing existing factories
public class ConcreteProductC : IProduct
{
public string GetName() => "Product C";
public void Display() => Console.WriteLine("This is Product C");
}
public class ConcreteCreatorC : Creator
{
public override IProduct CreateProduct()
{
return new ConcreteProductC();
}
}
Practical Examples
Example 1: Document Creation System
// Document interface
public interface IDocument
{
void Open();
void Save();
void Close();
}
// Concrete Documents
public class WordDocument : IDocument
{
public void Open() => Console.WriteLine("Opening Word document");
public void Save() => Console.WriteLine("Saving Word document");
public void Close() => Console.WriteLine("Closing Word document");
}
public class PdfDocument : IDocument
{
public void Open() => Console.WriteLine("Opening PDF document");
public void Save() => Console.WriteLine("Saving PDF document");
public void Close() => Console.WriteLine("Closing PDF document");
}
public class ExcelDocument : IDocument
{
public void Open() => Console.WriteLine("Opening Excel document");
public void Save() => Console.WriteLine("Saving Excel document");
public void Close() => Console.WriteLine("Closing Excel document");
}
// Document Factory
public abstract class DocumentFactory
{
public abstract IDocument CreateDocument();
public void ProcessDocument()
{
IDocument doc = CreateDocument();
doc.Open();
doc.Save();
doc.Close();
}
}
// Concrete Factories
public class WordDocumentFactory : DocumentFactory
{
public override IDocument CreateDocument() => new WordDocument();
}
public class PdfDocumentFactory : DocumentFactory
{
public override IDocument CreateDocument() => new PdfDocument();
}
public class ExcelDocumentFactory : DocumentFactory
{
public override IDocument CreateDocument() => new ExcelDocument();
}
// Usage
DocumentFactory factory = new WordDocumentFactory();
factory.ProcessDocument();
Example 2: Payment Processing System
public interface IPaymentProcessor
{
bool ProcessPayment(decimal amount);
string GetTransactionId();
}
public class CreditCardProcessor : IPaymentProcessor
{
public bool ProcessPayment(decimal amount)
{
Console.WriteLine(`Processing credit card payment: ${amount}`);
return true;
}
public string GetTransactionId() => `CC-${Guid.NewGuid()}`;
}
public class PayPalProcessor : IPaymentProcessor
{
public bool ProcessPayment(decimal amount)
{
Console.WriteLine(`Processing PayPal payment: ${amount}`);
return true;
}
public string GetTransactionId() => `PP-${Guid.NewGuid()}`;
}
public class CryptoProcessor : IPaymentProcessor
{
public bool ProcessPayment(decimal amount)
{
Console.WriteLine(`Processing cryptocurrency payment: ${amount}`);
return true;
}
public string GetTransactionId() => `CRYPTO-${Guid.NewGuid()}`;
}
public abstract class PaymentFactory
{
public abstract IPaymentProcessor CreateProcessor();
public bool ExecutePayment(decimal amount)
{
IPaymentProcessor processor = CreateProcessor();
bool success = processor.ProcessPayment(amount);
if (success)
{
Console.WriteLine(`Transaction ID: ${processor.GetTransactionId()}`);
}
return success;
}
}
public class CreditCardFactory : PaymentFactory
{
public override IPaymentProcessor CreateProcessor() => new CreditCardProcessor();
}
public class PayPalFactory : PaymentFactory
{
public override IPaymentProcessor CreateProcessor() => new PayPalProcessor();
}
public class CryptoFactory : PaymentFactory
{
public override IPaymentProcessor CreateProcessor() => new CryptoProcessor();
}
// Usage
PaymentFactory paymentFactory = new PayPalFactory();
paymentFactory.ExecutePayment(99.99m);
Example 3: Logger Factory with Configuration
public interface ILogger
{
void Log(string message);
void LogError(string message);
}
public class FileLogger : ILogger
{
private readonly string _filePath;
public FileLogger(string filePath)
{
_filePath = filePath;
}
public void Log(string message)
{
File.AppendAllText(_filePath, `[INFO] ${DateTime.Now}: ${message}\n`);
}
public void LogError(string message)
{
File.AppendAllText(_filePath, `[ERROR] ${DateTime.Now}: ${message}\n`);
}
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(`[INFO] ${message}`);
Console.ResetColor();
}
public void LogError(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(`[ERROR] ${message}`);
Console.ResetColor();
}
}
public class DatabaseLogger : ILogger
{
private readonly string _connectionString;
public DatabaseLogger(string connectionString)
{
_connectionString = connectionString;
}
public void Log(string message)
{
// Insert into database
Console.WriteLine(`Logging to database: ${message}`);
}
public void LogError(string message)
{
// Insert error into database
Console.WriteLine(`Logging error to database: ${message}`);
}
}
public abstract class LoggerFactory
{
public abstract ILogger CreateLogger();
}
public class FileLoggerFactory : LoggerFactory
{
private readonly string _filePath;
public FileLoggerFactory(string filePath)
{
_filePath = filePath;
}
public override ILogger CreateLogger() => new FileLogger(_filePath);
}
public class ConsoleLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger() => new ConsoleLogger();
}
public class DatabaseLoggerFactory : LoggerFactory
{
private readonly string _connectionString;
public DatabaseLoggerFactory(string connectionString)
{
_connectionString = connectionString;
}
public override ILogger CreateLogger() => new DatabaseLogger(_connectionString);
}
// Usage
LoggerFactory loggerFactory = new FileLoggerFactory("app.log");
ILogger logger = loggerFactory.CreateLogger();
logger.Log("Application started");
logger.LogError("An error occurred");
Example 4: Parameterized Factory Method
public enum VehicleType
{
Car,
Motorcycle,
Truck
}
public interface IVehicle
{
void Drive();
int GetCapacity();
}
public class Car : IVehicle
{
public void Drive() => Console.WriteLine("Driving a car");
public int GetCapacity() => 5;
}
public class Motorcycle : IVehicle
{
public void Drive() => Console.WriteLine("Riding a motorcycle");
public int GetCapacity() => 2;
}
public class Truck : IVehicle
{
public void Drive() => Console.WriteLine("Driving a truck");
public int GetCapacity() => 3;
}
public class VehicleFactory
{
public static IVehicle CreateVehicle(VehicleType type)
{
return type switch
{
VehicleType.Car => new Car(),
VehicleType.Motorcycle => new Motorcycle(),
VehicleType.Truck => new Truck(),
_ => throw new ArgumentException("Invalid vehicle type")
};
}
}
// Usage
IVehicle vehicle = VehicleFactory.CreateVehicle(VehicleType.Car);
vehicle.Drive();
Console.WriteLine(`Capacity: ${vehicle.GetCapacity()} passengers`);
Advantages of Factory Method Pattern
- Loose Coupling: Client code doesn't depend on concrete classes
- Single Responsibility: Object creation logic is centralized
- Open/Closed Principle: Easy to add new product types without modifying existing code
- Testability: Easy to mock factories for unit testing
- Flexibility: Subclasses can override factory methods to change product types
- Code Reusability: Common creation logic can be shared across factories
Disadvantages and Limitations
- Increased Complexity: Requires creating multiple classes and interfaces
- More Code: More boilerplate code compared to direct instantiation
- Indirection: Can make code harder to follow for simple scenarios
- Overhead: Additional abstraction layers may impact performance slightly
- Over-Engineering: May be overkill for simple object creation
When to Use Factory Method Pattern
Use the Factory Method pattern when:
- You don't know beforehand the exact types and dependencies of objects your code should work with
- You want to provide users of your library or framework with a way to extend its internal components
- You want to save system resources by reusing existing objects instead of rebuilding them
- You need to decouple object creation from object usage
- You want to centralize object creation logic
- You need to support multiple product families
- Your code needs to work with different types of objects that share a common interface
Avoid the Factory Method pattern when:
- Object creation is simple and unlikely to change
- You only have one concrete product type
- The added complexity doesn't provide clear benefits
- Performance is critical and the abstraction overhead is unacceptable
Performance Impact
Performance Considerations
- Virtual Method Calls: Factory methods are typically virtual, adding slight overhead
- Indirection: Additional method calls add minimal latency
- Memory: More classes mean slightly more memory usage
- JIT Optimization: Modern JIT compilers can inline simple factory methods
// Performance comparison (approximate)
public class PerformanceBenchmark
{
// Direct instantiation - fastest
public void DirectCreation()
{
var product = new ConcreteProductA(); // ~1-2ns
}
// Factory Method - minimal overhead
public void FactoryCreation()
{
Creator factory = new ConcreteCreatorA();
var product = factory.CreateProduct(); // ~3-5ns
}
// Parameterized Factory - slightly more overhead
public void ParameterizedFactory()
{
var product = VehicleFactory.CreateVehicle(VehicleType.Car); // ~5-10ns
}
}
Performance Optimization Tips
- Object Pooling: Combine with object pooling for frequently created objects
- Lazy Initialization: Create objects only when needed
- Caching: Cache factory instances if they're stateless
- Sealed Classes: Mark concrete products as sealed for better JIT optimization
// Optimized factory with caching
public class OptimizedFactory
{
private static readonly Dictionary _cache = new();
private static readonly object _lock = new();
public static IProduct GetProduct(string type)
{
if (_cache.TryGetValue(type, out var cached))
return cached;
lock (_lock)
{
if (_cache.TryGetValue(type, out cached))
return cached;
IProduct product = CreateNewProduct(type);
_cache[type] = product;
return product;
}
}
private static IProduct CreateNewProduct(string type)
{
return type switch
{
"A" => new ConcreteProductA(),
"B" => new ConcreteProductB(),
_ => throw new ArgumentException("Unknown type")
};
}
}
Factory Method vs Other Patterns
Factory Method vs Abstract Factory
- Factory Method: Creates one product type, uses inheritance
- Abstract Factory: Creates families of related products, uses composition
Factory Method vs Builder
- Factory Method: Creates objects in one step
- Builder: Constructs complex objects step by step
Factory Method vs Prototype
- Factory Method: Creates new objects from scratch
- Prototype: Clones existing objects
Best Practices
- Use Interfaces: Define product interfaces for maximum flexibility
- Keep Factories Simple: Factory methods should focus on object creation only
- Consider Dependency Injection: Combine with DI containers for better testability
- Document Factory Behavior: Clearly document what each factory creates
- Use Meaningful Names: Factory class names should clearly indicate what they create
- Validate Parameters: Always validate input parameters in parameterized factories
- Handle Errors Gracefully: Provide clear error messages for invalid inputs
Real-World Applications
- ASP.NET Core: Uses factory pattern for creating middleware and services
- Entity Framework: DbContext factory for creating database contexts
- Logging Frameworks: LoggerFactory in Microsoft.Extensions.Logging
- UI Frameworks: Creating different UI controls based on platform
- Game Development: Creating different enemy types, weapons, or power-ups
Further Reading and Resources
- Refactoring Guru: Factory Method
- Microsoft: .NET Design Guidelines
- DoFactory: Factory Method Pattern
- Design Patterns: Elements of Reusable Object-Oriented Software (Gang of Four)
- Head First Design Patterns
The Factory Method pattern is a fundamental design pattern that promotes loose coupling and flexibility in object creation. While it adds some complexity, the benefits in maintainability and extensibility make it invaluable for building scalable applications.


