Factory Pattern: Dynamically Creating Objects in C# | Ultimate Guide

Factory Pattern in C# - Essential Guide

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# (10,000+ Words) | Design Patterns

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

Post a Comment (0)

Previous Post Next Post

ads

ads

Update cookies preferences