Demystifying the Factory Pattern in C#

Demystifying the Factory Pattern in C#

The Factory Pattern is categorized under the creational design patterns, and provides an interface for creating objects but allows subclasses to alter the type of objects that will be created. In essence, it abstracts the process of object creation and decouples the client code from the specific classes it instantiates.

Understanding the Factory Pattern

At its core, the Factory Pattern defines an interface or an abstract class for creating objects, but it does not specify the concrete classes to instantiate. Instead, the responsibility for creating objects is delegated to the derived classes, which implement the Factory Method.

Here are the key components of the Factory Pattern:

Product: This is the common interface or abstract class that defines the objects to be created by the factory.

Concrete Product: These are the actual classes that implement the Product interface or inherit from the Product abstract class.

Factory: The factory is an interface or an abstract class that declares a method for creating objects. This is the core of the Factory Pattern.

Concrete Factory: These are the classes that implement the factory's creation method, returning instances of Concrete Products. Each concrete factory is responsible for creating a specific type of product.

Client: The client code uses the factory to create objects without having to know the specific classes that are being instantiated. This promotes loose coupling between the client code and the products.

Implementing the Factory Pattern in C

Let's walk through a simple example to demonstrate the Factory Pattern in C#. Consider a scenario where you have different types of message brokers like Azure, AWS, and Google Cloud vendors.

// Step 1: Define the Product interface or abstract class
public interface IMessageBroker
{
    void SendMessage();
    void ReceiveMessage();
}
// Step 2: Implement Concrete Products
public class AwsMessageBroker : IMessageBroker
{
    private readonly ILogger logger;
    public AwsMessageBroker(ILogger<AwsMessageBroker> logger)
    {
        this.logger = logger;
    }
    public void ReceiveMessage()
    {
        logger.LogInformation("Recieve message from AWS");
    }

    public void SendMessage()
    {
        logger.LogInformation("Send message to AWS");
    }
}
public class AzureMessageBroker : IMessageBroker
{
    private readonly ILogger logger;
    public AzureMessageBroker(ILogger<AwsMessageBroker> logger)
    {
        this.logger = logger;
    }
    public void ReceiveMessage()
    {
        logger.LogInformation("Recieve message from Azure");
    }

    public void SendMessage()
    {
        logger.LogInformation("Send message to Azure");
    }
}
// Step 3: Create the Factory interface or abstract class
public interface IMessageBrokerFactory
{
    public IMessageBroker GetMessageBroker(Brokers brokers);
}
// Step 4: Implement Concrete Factories
public class MessageBrokerFactory : IMessageBrokerFactory
{
    private readonly IServiceProvider serviceProvider;
    public MessageBrokerFactory(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }
    public IMessageBroker GetMessageBroker(Brokers brokers)
    {
        switch (brokers)
        {
            case Brokers.Azure:
                return (IMessageBroker) serviceProvider.GetService(typeof(AzureMessageBroker));
            case Brokers.AWS:
                return (IMessageBroker) serviceProvider.GetService(typeof(AwsMessageBroker));
            case Brokers.Google:
                return (IMessageBroker)serviceProvider.GetService(typeof(GcpMessageBroker));
            default:
                throw new ArgumentOutOfRangeException(nameof(brokers), brokers, $"Broker {brokers} is not supported.");
        }
    }
}
// Step 5: Set Dependency injection services
builder.Services.AddControllers();
builder.Services.AddScoped<IMessageBrokerFactory, MessageBrokerFactory>();

builder.Services.AddScoped<AwsMessageBroker>();
builder.Services.AddScoped<IMessageBroker, AwsMessageBroker>(s => s.GetService<AwsMessageBroker>());

builder.Services.AddScoped<AzureMessageBroker>();
builder.Services.AddScoped<IMessageBroker, AzureMessageBroker>(s => s.GetService<AzureMessageBroker>());

builder.Services.AddScoped<GcpMessageBroker>();
builder.Services.AddScoped<IMessageBroker, GcpMessageBroker>(s => s.GetService<GcpMessageBroker>());
// Step 6: Client code
messageBrokerFactory.GetMessageBroker(Providers.Brokers.AWS).SendMessage();
messageBrokerFactory.GetMessageBroker(Providers.Brokers.Azure).SendMessage();
messageBrokerFactory.GetMessageBroker(Providers.Brokers.Google).SendMessage();

In this example, we have:

  • Defined a MessageBrokerinterface as the product.

  • Implemented three concrete products: Aws, Azureand Google.

  • Created a MessageBrokerFactory interface and one concrete factory: responsible for creating a specific type of broker.

  • In the client code, we use the factory to create a three brokers without knowing the specific broker type being created.

The Factory Pattern enables you to extend the system by adding new broker types (concrete products) and factories without modifying existing client code. It also promotes code reusability and flexibility.

Benefits of the Factory Pattern

The Factory Pattern provides several advantages in software design:

  1. Encapsulation: The creation logic is encapsulated within the factory classes, abstracting it from the client code. This promotes information hiding and reduces dependencies.

  2. Flexibility and Extensibility: You can easily introduce new product types and factories without modifying the existing code. This is crucial for maintaining and extending complex software systems.

  3. Consistency: By using factories, you ensure that objects are created consistently throughout your application.

  4. Testing: It simplifies unit testing by allowing you to replace actual products with mock objects, making testing easier and more effective.

Drawbacks and Considerations

While the Factory Pattern offers many benefits, it may not be suitable for every situation. Some considerations include:

  1. Complexity: For simple object creation scenarios, the Factory Pattern might introduce unnecessary complexity. In such cases, direct object instantiation could be more straightforward.

  2. Abstraction Overhead: Introducing the pattern can add an additional layer of abstraction. This might be overkill for simple projects.

  3. Maintenance: You need to maintain the factory classes, which can lead to additional maintenance efforts as the project evolves.

  4. Factory Explosion: In large projects with many products, you might end up with a large number of concrete factories, potentially leading to what's known as a "factory explosion." This can make the codebase harder to manage.

Conclusion

The Factory Pattern is a powerful tool in your software design arsenal, especially in situations where you need to abstract and centralize object creation. By encapsulating the creation logic, it promotes maintainability, flexibility, and loose coupling in your code. It's essential to use this pattern judiciously, considering the specific requirements of your project. When applied appropriately, the Factory Pattern can greatly enhance the structure and scalability of your C# applications.

Did you find this article valuable?

Support Guillermo Valenzuela's blog by becoming a sponsor. Any amount is appreciated!