Factory Pattern in C#
The Factory Pattern is a creational design pattern that provides a way to create objects without exposing the instantiation logic.
Why Use Factory Pattern?
- Decouples object creation from usage
- Centralizes creation logic
- Makes code more maintainable
- Supports Open/Closed Principle
1. Simple Factory
public interface IVehicle { void Drive(); }
public class Car : IVehicle { public void Drive() => Console.WriteLine("Driving car"); }
public class VehicleFactory {
public IVehicle Create(string type) {
return type switch {
"car" => new Car(),
_ => throw new ArgumentException("Invalid type")
};
}
}
2. Factory Method
public abstract class VehicleCreator {
public abstract IVehicle Create();
public void Operate() => Create().Drive();
}
public class CarCreator : VehicleCreator {
public override IVehicle Create() => new Car();
}
3. Abstract Factory
public interface IUIFactory {
IButton CreateButton();
ITextBox CreateTextBox();
}
public class WindowsFactory : IUIFactory {
public IButton CreateButton() => new WindowsButton();
public ITextBox CreateTextBox() => new WindowsTextBox();
}
When to Use Each
Pattern | When to Use |
---|---|
Simple Factory | Basic scenarios with few types |
Factory Method | When subclassing is needed |
Abstract Factory | Creating families of related objects |
Tip: Combine Factory Pattern with Dependency Injection for best results in modern applications.
Complete Guide to Factory Pattern in C#
1. Introduction to Factory Pattern
The Factory Pattern is a creational design pattern that provides an interface for creating objects while allowing subclasses to alter the type of objects that will be created. It's one of the most widely used design patterns in object-oriented programming.
1.1 The Problem Factory Pattern Solves
Consider a scenario where you need to create different types of database connections:
// Without factory - problematic approach
IDatabaseConnection connection;
if (dbType == "sqlserver")
{
connection = new SqlServerConnection(connectionString);
}
else if (dbType == "oracle")
{
connection = new OracleConnection(connectionString);
}
// More conditions as new database types are added
This approach has several issues:
- Violates Open/Closed Principle (must modify when adding new types)
- Creation logic is scattered throughout the application
- Hard to mock for unit testing
2. Types of Factory Patterns
2.1 Simple Factory Pattern
The simplest form that encapsulates object creation:
public class DatabaseFactory
{
public IDatabaseConnection CreateConnection(string dbType, string connectionString)
{
return dbType.ToLower() switch
{
"sqlserver" => new SqlServerConnection(connectionString),
"oracle" => new OracleConnection(connectionString),
"mysql" => new MySqlConnection(connectionString),
_ => throw new ArgumentException("Invalid database type")
};
}
}
// Usage
var factory = new DatabaseFactory();
var connection = factory.CreateConnection("sqlserver", connectionString);
Note: While not officially a GoF pattern, Simple Factory is widely used for its simplicity in small projects.
2.2 Factory Method Pattern
Defers instantiation to subclasses:
public abstract class DatabaseCreator
{
public abstract IDatabaseConnection CreateConnection(string connectionString);
public void TestConnection(string connectionString)
{
var connection = CreateConnection(connectionString);
connection.Open();
connection.Test();
connection.Close();
}
}
public class SqlServerCreator : DatabaseCreator
{
public override IDatabaseConnection CreateConnection(string connectionString)
{
return new SqlServerConnection(connectionString);
}
}
// Usage
DatabaseCreator creator = new SqlServerCreator();
creator.TestConnection(connectionString);
2.3 Abstract Factory Pattern
Creates families of related objects:
public interface IDatabaseFactory
{
IDatabaseConnection CreateConnection();
IQueryBuilder CreateQueryBuilder();
IDataAdapter CreateDataAdapter();
}
public class SqlServerFactory : IDatabaseFactory
{
public IDatabaseConnection CreateConnection() => new SqlServerConnection();
public IQueryBuilder CreateQueryBuilder() => new SqlServerQueryBuilder();
public IDataAdapter CreateDataAdapter() => new SqlServerDataAdapter();
}
// Client code
public class DatabaseClient
{
private readonly IDatabaseConnection _connection;
private readonly IQueryBuilder _queryBuilder;
public DatabaseClient(IDatabaseFactory factory)
{
_connection = factory.CreateConnection();
_queryBuilder = factory.CreateQueryBuilder();
}
public void ExecuteQuery(string sql)
{
_connection.Open();
var command = _queryBuilder.BuildCommand(sql);
command.Execute();
_connection.Close();
}
}
3. Advanced Factory Techniques
3.1 Generic Factories
public interface IGenericFactory
{
T Create();
}
public class GenericFactory : IGenericFactory where T : new()
{
public T Create() => new T();
}
// Registration with DI
services.AddTransient(typeof(IGenericFactory<>), typeof(GenericFactory<>));
// Usage
public class ProductService
{
private readonly IGenericFactory _productFactory;
public ProductService(IGenericFactory productFactory)
{
_productFactory = productFactory;
}
public Product CreateProduct() => _productFactory.Create();
}
3.2 Async Factories
public interface IAsyncDatabaseFactory
{
Task CreateConnectionAsync(string connectionString);
}
public class SqlServerAsyncFactory : IAsyncDatabaseFactory
{
public async Task CreateConnectionAsync(string connectionString)
{
var connection = new SqlServerConnection(connectionString);
await connection.InitializeAsync();
return connection;
}
}
// Usage
var factory = new SqlServerAsyncFactory();
var connection = await factory.CreateConnectionAsync(connectionString);
4. Real-World Examples
4.1 Payment Processor Factory
public interface IPaymentProcessor
{
Task ProcessPaymentAsync(PaymentRequest request);
}
public class PaymentProcessorFactory : IPaymentProcessorFactory
{
private readonly IServiceProvider _serviceProvider;
public PaymentProcessorFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IPaymentProcessor Create(string paymentMethod)
{
return paymentMethod.ToLower() switch
{
"creditcard" => _serviceProvider.GetRequiredService(),
"paypal" => _serviceProvider.GetRequiredService(),
"applepay" => _serviceProvider.GetRequiredService(),
_ => throw new ArgumentException("Unsupported payment method")
};
}
}
// Registration
services.AddScoped();
services.AddScoped();
services.AddSingleton();
// Usage in controller
public class PaymentController : Controller
{
private readonly IPaymentProcessorFactory _factory;
public PaymentController(IPaymentProcessorFactory factory)
{
_factory = factory;
}
[HttpPost]
public async Task ProcessPayment([FromBody] PaymentRequest request)
{
var processor = _factory.Create(request.PaymentMethod);
var result = await processor.ProcessPaymentAsync(request);
return Ok(result);
}
}
5. Performance Considerations
Approach | Performance Impact | When to Use |
---|---|---|
Simple Factory | Low overhead | When creation logic is simple |
Factory Method | Medium overhead | When polymorphism is needed |
Abstract Factory | Higher overhead | When creating object families |
Cached Factory | Best performance | When objects are expensive to create |
Cached Factory Example
public class CachedVehicleFactory
{
private readonly ConcurrentDictionary _cache = new();
public IVehicle GetVehicle(string type)
{
return _cache.GetOrAdd(type, t =>
{
return t.ToLower() switch
{
"car" => new Car(),
"truck" => new Truck(),
_ => throw new ArgumentException("Invalid vehicle type")
};
});
}
}
Post a Comment