Design Patterns & Practices  

Factory Method Pattern in C#

The Factory Pattern is a creational design pattern that provides a centralized place to creating an objects without exposing the object creation logic to the client.

The client requests a factory for an object, and the factory decides which concrete class to instantiate. Instead of using new keyword to create a object, object creation will be delegated to the factory.

When the system grows, one of the first design problems we face is object creation. Tight coupling between object creation and object usage makes code hard to extend, test, and maintain. That’s where the Factory Pattern becomes extremely useful.

Prerequisites

  • Visual Studio 2022 installed,

  • Basics of .NET

Source Code

The source code used in the example is publicly available on github: Link

Why do we need the factory pattern?

Let’s take a simple example

var payment = new CreditCardPayment();
payment.Pay();

The above code example has the following issues:

  • Tight coupling with a concrete class

  • Difficult to extend (new payment types require code changes everywhere)

  • Violates Open/Closed Principle

Now imagine adding other payment methods like DebitCard, PayPal etc. we will be ended up having else-if ladder or switch statement accross the codebase.

Solution With Factory Pattern

The Factory Pattern will helps to:

  • Encapsulates object creation

  • Reduces coupling

  • Makes the system open for extension but closed for modification

Step-by-Step implementation with real world example

Let’s consider the real-world payment processing scenario and combine it with the dependency injection to make a program loosely coupled.

Step 1: Create a Payment Interface

public interface IPaymentMethod
{
    string Method { get; }
    void Pay(decimal amount);
}

Step 2: Write concrete implementation for Credit Card, Debit Card, PayPal

Credit Card Payment Class

public class CreditCardPayment : IPaymentMethod
{
    public string Method => "credit";

    public void Pay(decimal amount)
    {
        Console.WriteLine($"Paid {amount} using Credit Card");
    }
}

Debit Card Payment Class

public class DebitCardPayment : IPaymentMethod
{
    public string Method => "debit";

    public void Pay(decimal amount)
    {
        Console.WriteLine($"Paid {amount} using Debit Card");
    }
}

PayPal Payment Class

public class PaypalPayment : IPaymentMethod
{
    public string Method => "paypal";

    public void Pay(decimal amount)
    {
        Console.WriteLine($"Paid {amount} using PayPal");
    }
}

Step 3: Crate a Factory Interface IPaymentFactory

public interface IPaymentFactory
{
    IPaymentMethod Create(string method);
}

Step 4: Factory Implementation PaymentFactory (with no Coupling)

using System;
using System.Collections.Generic;
using System.Linq;

public class PaymentFactory : IPaymentFactory
{
    private readonly IDictionary<string, IPaymentMethod> _paymentMethods;

    public PaymentFactory(IEnumerable<IPaymentMethod> paymentMethods)
    {
        _paymentMethods = paymentMethods.ToDictionary(
            p => p.Method,
            p => p,
            StringComparer.OrdinalIgnoreCase);
    }

    public IPaymentMethod Create(string method)
    {
        if (_paymentMethods.TryGetValue(method, out var payment))
            return payment;

        throw new NotSupportedException($"Payment method '{method}' is not supported");
    }
}

Step 5: Program.cs

using FactoryPatternWithDI.Factories;
using FactoryPatternWithDI.Payments;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

// Register payment implementations
services.AddTransient<IPaymentMethod, CreditCardPayment>();
services.AddTransient<IPaymentMethod, DebitCardPayment>();
services.AddTransient<IPaymentMethod, PaypalPayment>();

// Register factory
services.AddSingleton<IPaymentFactory, PaymentFactory>();

var provider = services.BuildServiceProvider();

var factory = provider.GetRequiredService<IPaymentFactory>();

Console.WriteLine("Choose payment method: credit / debit / paypal");
var method = Console.ReadLine();

var payment = factory.Create(method);
payment.Pay(1500);

Console.ReadLine();

Conclusion

In the above example the client code Program.cs never creates a concrete payment object using a new keyword. Instead, it requests a payment method from IPaymentFactory. Each of the payment types like CreditCardPayment, DebitCardPayment, PaypalPayment implements the common IPaymentMethod and self-identifies using a method key.

Let’s analyze the above implementation via design-principle perspective:

  • Single Responsibility Principle: Here payment classes handles the payment logic, the factory handles the selection and client handles the orchestration.

  • Open/Close Principle: New payment method can be added without changing existing factory or client implementation code. We can add StripePayment without touching Program.cs or PaymentFactory

  • Dependency Inversion Principle: Here high level modules like PaymentFactory and Program.cs is dependent on the abstaction not on the concrete implementation class like CreditCardPayment, DebitCardPayment or PaypalPayment classes.