The Factory Method Pattern — Design Patterns

Mahmut Kasimoglu
6 min readFeb 11, 2024

--

The Factory Method Pattern, Is a simple yet super powerful design pattern if used correctly in the right places. It provides an interface for object creation which allows for a loosely coupled design. It is a perfect way to introduce changes without causing any breaking changes, so the client code will have nothing to worry about and can continue to consume a service as usual.

This Pattern like most others also conforms to the Open Closed Principle (OCP) by allowing us to abstract the creation of objects to the sub-classes so the main code that uses this does not know the implementation but just has to know its signature. Additionally, this pattern also conforms to the Single Responsibility Principle (SRP) again for the reason above, as we are designing a class or classes to only handle the creation of an instance for a given type.

To make everything I mentioned above make more sense, we are going to be looking at a simple scenario that consists of building an online payment feature, where we first start with one payment method and later start adding more.

Problem

Imagine that the company you work for wants a system that supports payment through third-party services. At first, only payments through PayPal are allowed. You are told to build a feature to facilitate this. So then you start working on it.

 public class PaymentManager : IPaymentManager
{
public async void ExecutePayment(int amount)
{

//Sample Business Logic, in this case a Validation.
if (amount <= 0)
{
throw new InvalidOperationException("\"Amount must be more than 0.\"");
}

var invoice = await ProcessPaymentAsync(amount);
Console.WriteLine($"Payment Complete");
Console.WriteLine($"Amount Paid: {invoice.Amount}");
Console.WriteLine($"Transaction Id: {invoice.TransactionId}");
}

//Method that facilitates the payment process.
private async Task<Invoice> ProcessPaymentAsync(double amount)
{
//Connection Logic With PayPal Service goes Here, and we get example Guid from the Api.
return new Invoice(Guid.NewGuid(), amount);
}
}

You come up with the solution above, and everything works fine. But as time passes your boss tells you that he wants to allow more kinds of payment methods. This time he asks you to upgrade the feature to also allow payment through Credit Card. Now you have to start making changes to the code above to facilitate this, let’s take a look at how we can do this, the wrong way.

  public class PaymentManager
{
//We added a new parameter caled paymentType, to determine the type of payment
public async void ExecutePayment(int amount, string paymentType)
{

//Sample Business Logic within PaymentManager.
if (amount <= 0)
{
throw new InvalidOperationException("\"Amount must be more than 0.\"");
}
//we pass the paymentType here.
var invoice = await ProcessPaymentAsync(amount, paymentType);
Console.WriteLine($"Payment Complete");
Console.WriteLine($"Amount Paid: {invoice.Amount}");
Console.WriteLine($"Transaction Id: {invoice.TransactionId}");
}


private async Task<Invoice> ProcessPaymentAsync(double amount, string paymentType)
{
Guid transactionId;

//We recieve the paymentType and hit the corresponding service
switch (paymentType)
{
case "PayPal":
transactionId = Guid.NewGuid(); //Simulation of getting Guid from
//Paypal service after a succesful transaction
break;
case "CreditCard":
transactionId = Guid.NewGuid(); //same here
break;
default:
throw new ArgumentException("Unsupported payment type.", nameof(paymentType));
}

//return the invoice
return new Invoice(transactionId, amount);
}
}

See the problem here? Let's first take a look at the things that we have changed/added, First of all, we added a new parameter to support different types of payment methods, we then passed this new parameter to ProcessPaymentAsync() which also has a new switch statement added that determines the service to call according to the paymentType. Let's list the problems of these changes.

  1. We have introduced breaking changes, this is because the clients that use the paymentManager have to be updated to be compatible with the new method signature.
  2. We are breaking the rule of the Open Closed Principle (OCP) by modifying the underlying code instead of extending it.
  3. We are coupling the logic of calling the services to this class. Imagine if there were 20 payment methods in the future. I am sure you can visualize how ugly the code would look, let alone the complexity of it. Now Imagine testing this class at that point, yup don’t even want to think about it, YIKES.

So what is the solution? Well, that’s what the topic of the article today is about, The Factory Method Pattern. The Strategy Pattern would be suitable for this problem too, but the Factory Method, in this case, is at least as good or even a better fit for this! Let’s take a look at how we can approach this problem using it.

Solution


public abstract class BasePaymentManager
{
public async void ExecutePayment(int amount)
{

//Sample Business Logic within PaymentManager.
if (amount <= 0)
{
throw new InvalidOperationException("\"Amount must be more than 0.\"");
}

var paymentProcessor = GetPaymentProcessor();

var invoice = await paymentProcessor.ProcessPaymentAsync(amount);
Console.WriteLine($"Payment Complete, Payment Type: {invoice.PaymentType}");
Console.WriteLine($"Amount Paid: {invoice.Amount}");
Console.WriteLine($"Transaction Id: {invoice.TransactionId}");
}

//We leave this implementation to the Subclass.
protected abstract IPaymentProcessor GetPaymentProcessor();
}


public class PayPalPaymentManager : BasePaymentManager
{
protected override IPaymentProcessor GetPaymentProcessor()
{

//Additional Creation Logic can be placed here.

return new PaypalPaymentProcessor();
}
}

public class CreditCardPaymentManager : BasePaymentManager
{
protected override IPaymentProcessor GetPaymentProcessor()
{

//Additional Creation Logic can be placed here.

return new CreditCardPaymentProcessor();
}
}


public interface IPaymentProcessor
{
Task<Invoice> ProcessPaymentAsync(double amount);
}

public class PaypalPaymentProcessor : IPaymentProcessor
{
public async Task<Invoice> ProcessPaymentAsync(double amount)
{

//Connection Logic With PayPal Api Here and we get example Guid from the Api.
var guid = Guid.NewGuid();

return new Invoice(guid, amount, PaymentType.PayPal);
}
}

public class CreditCardPaymentProcessor : IPaymentProcessor
{
public async Task<Invoice> ProcessPaymentAsync(double amount)
{

//Connection Logic With CreditCard Api Here and we get example Guid from the Api.
var guid = Guid.NewGuid();

return new Invoice(guid, amount, PaymentType.CreditCard);
}
}

public record Invoice(Guid TransactionId, double Amount, PaymentType PaymentType);

public enum PaymentType
{
PayPal,
CreditCard
}


// usage
BasePaymentManager paymentManager = new PayPalPaymentManager();
paymentManager.ExecutePayment(50);
paymentManager = new CreditCardPaymentManager();
paymentManager.ExecutePayment(120);

//Console Output
//Payment Complete, Payment Type: PayPal
//Amount Paid: 50
//Transaction Id: 2359fb23-70f1-4c6b-a1e6-87a6ce100f5a
//Payment Complete, Payment Type: Wise
//Amount Paid: 120
//Transaction Id: 5d82d1db-3c8a-43d8-ac54-da27de8107c1

So what have we done?

  1. We created a new interface called IPaymentProcessor with a method signature called ProcessPaymentAsync which returns an Invoice. after that, we created its concrete classes, PayPalPaymentProcessor and CreditCardPaymentProcessor.
  2. We renamed our PaymentManager class to BasePaymentManager and also changed it from a normal class to an abstract class, to allow GetPaymentProcessor to be abstract and implemented by subclasses.
  3. We created two sub PaymentManager classes that inherit from the BasePaymentManager class, these are PayPalPaymentManager and CreditCardPaymentManager. these classes Implement the GetPaymentProcessor method which returns an instance of the type IPaymentProcessor.
  4. We created an Enum to display the payment method when we print to the console.

This way, We have built an architecture that is super flexible and decoupled. The Base class does not know what type of IPaymentProcessor the GetPaymentProcessor method is going to return, it just knows that it is going to return a type of IPaymentProcessor and that it has the signature method it needs to make a call to. It leaves the implementation of the GetPaymentProcessor to the sub-classes.

We are also now conforming to both SRP and the OCP as Each base PaymentManager class is only responsible for creating the PaymentProcessor classes.

We also no longer need to modify the same class when wanting to add any new Payment methods, we simply just have to create the new corresponding classes which in turn leads to no more breaking changes like we previously did.

How to decide when to use the Factory Method Pattern?

Use this pattern when you have a class that has most of its operations do the same thing (like in the case above, we had a validation process before we processed the payment) but in it, there is a part where its behavior varies based on specific conditions or scenarios. Also, something to note is that it is best to use this pattern in scenarios where there is only one way at a time to perform an operation that varies.

What I mean by that is, in our case, we are allowing users to choose a payment method, and it is very unlikely that a user will want to make a payment using two different methods at a time, they will just need one, Either Paypal or a Credit Card. So this makes the factory method pattern a perfect fit.

Now let's take a look at what scenario may not be a well fit for this pattern. for instance, in a notification sending system, A user may choose to send notifications via more than one way at a time, through SMS and Email, In that case, this pattern would not be a good fit because then we would have to create classes for all possible combinations with the notification types that exist, which would lead to a class explosion. So it is better to use patterns such as the Decorator Pattern in that case, this way we can choose at run-time what collection of medium to use when sending a notification.

--

--